iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 25
0
Modern Web

我不用Expo啦,React Native!系列 第 25

[Day25] 語言切換-2:store架構調整

今日關鍵字:combineReducers


預想中這個App會有兩個地方需要讀取語言

  1. 剛打開App時讀取Async Storage裡讀取使用者上次使用的語系,並且進行語系切換
  2. 設定頁面中顯示當前語系並給予切換語系功能

由於設定頁面與導覽列或者首頁並非相鄰元件,比起使用props傳遞,這裡選擇以redux進行操作


現在的store的結構長這樣

{
  allAnime: [...]
}

加入語言後應該變成

{
  allAnime: [...],
  language: 'xx'
}

如果直接更改reducer的內容
會出現一個語意上的問題
我的reducer或action叫做AnimeReducer ,AnimeAction…等
塞進對language的操作看起來...怪怪的/images/emoticon/emoticon13.gif
這裡我選擇新增另一組reducer及action,並使用combineReducers組合

參數為一個物件,寫法是

combineReducers({ reducerA, reducerB })

等價於

combineReducers({ reducerA: reducerA, reducerB: reducerB })

這裡當然可以更換key值

combineReducers({ keyA: reducerA, keyB: reducerB })

這裡我當然選擇更換key值的版本

combineReducers(
  { 
    allAnime: AnimeReducer, 
    language: languageReducer 
  }
)

代入AnimeReducer的狀態後

combineReducers(
  { 
    allAnime: {
      allAnime: [...]
    }, 
    language: languageReducer 
  }
)

結果跟預想的不同,多了一層allAnime

也就是說,combineReducers會使得store的結構多包一層
所以這裡需要把AnimeReducer的結構從

{
  allAnime: [...]
}

去掉一層變成

[...]

同樣的道理,新寫的language部分也不用以物件的方式,而應該是以字串的形式

Anime

調整初始值與return的結構

const initState: Array<Anime> = []

const animeReducer = (state = initState, action: animeActions.AnimeAction) => {
  const allAnimeCopy = [...state]
  const Favorite: Array<Anime> = []

  switch (action.type) {
    case animeActions.GET_ALL_ANIMATE_SUCCESS:
      return action.payload?.allAnime
    case animeActions.RENEW_DATA:
      for (let i = 0; i < state.length; i += 1) {
        if (action.payload!.anime!.id === allAnimeCopy[i].id) {
          allAnimeCopy[i] = action.payload!.anime!
        }
        if (allAnimeCopy[i].isFavorite || allAnimeCopy[i].isReminding) {
          Favorite.push(allAnimeCopy[i])
        }
      }
      storeFavorite(Favorite)
      return allAnimeCopy

Language

  • 新增儲存進Async Storage的函式
export const getLanguage = async () => {
  try {
    const lang = await AsyncStorage.getItem('language')
    return lang !== null ? JSON.parse(lang) : 'zh-TW' // 預設使用繁體中文
  } catch (error) {
    return 'zh-TW'
  }
}

export const storeLanguage = async (language: string) => {
  try {
    const jsonLanguage = JSON.stringify(language)
    await AsyncStorage.setItem('language', jsonLanguage)
  } catch (e) {
    // saving error
  }
}
  • 新增action及saga
import { Action } from 'redux'
import { call, put, takeEvery } from 'redux-saga/effects'

import { getLanguage } from '../../data/local'

export interface LanguageAction extends Action {
  type: string
  payload?: {
    language?: string
  }
}

export const RENEW_LANGUAGE = 'RENEW_LANGUAGE'

export const renewLanguage = (language: string) => ({
  type: RENEW_LANGUAGE,
  payload: {
    language
  }
})

export const GET_LANGUAGE_BEGIN = 'GET_LANGUAGE_BEGIN'
export const getLanguageBegin = () => ({
  type: GET_LANGUAGE_BEGIN
})

export const GET_LANGUAGE_SUCCESS = 'GET_LANGUAGE_SUCCESS'
export const getLanguageSuccess = (language: string) => ({
  type: GET_LANGUAGE_SUCCESS,
  payload: {
    language
  }
})

function* getLocalLanguage() {
  const language = yield call(() => getLanguage().then((result) => result))

  yield put(getLanguageSuccess(language))
}

function* languageSaga() {
  yield takeEvery(GET_LANGUAGE_BEGIN, getLocalLanguage)
}

export default languageSaga
  • rootSaga中新增languageSaga
import { all } from 'redux-saga/effects'
import animeSaga from '../action/animeAction'
import languageSaga from '../action/languageAction'

function* rootSaga() {
  yield all([animeSaga(), languageSaga()])
}

export default rootSaga
  • 新增reducer
import * as LanguageActions from '../action/languageAction'
import { storeLanguage } from '../../data/local'

const initState = 'zh-TW'

const languageReducer = (
  state = initState,
  action: LanguageActions.LanguageAction
) => {
  switch (action.type) {
    case LanguageActions.GET_LANGUAGE_SUCCESS:
      return action.payload!.language
    case LanguageActions.RENEW_LANGUAGE:
      storeLanguage(action.payload!.language!)
      return action.payload!.language
    default:
      return state
  }
}

export default languageReducer

合體

新增rootReducer

import { combineReducers } from 'redux'

import animeReducer from './animeReducer'
import languageReducer from './languageReducer'
import loadingReducer from './loadingReducer'
import { Anime } from '../../data/content'

export interface RootStateType {
  allAnime: Array<Anime>
  language: string
  isLoading: boolean
}

const rootReducer = combineReducers({
  allAnime: animeReducer,
  language: languageReducer,
  isLoading: loadingReducer
})

export default rootReducer

store中改使用rootReducer

...
// import animeReducer from '../reducer/animeReducer'
import rootReducer from '../reducer/rootReducer'

const store = createStore(
  // animeReducer,
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware))
)
...

store的調整到此結束
最後要選擇在哪個元件中進行語言的初次讀取及切換
選擇的條件有

  1. Provider內(才能讀取store內的狀態)
  2. 放在首先會被渲染的元件中,且盡量上層
  3. 由於useTranslationhook,需要在Function Component中呼叫
    基於以上理由,我選擇在底部的導覽列中進行語言的初次讀取及切換
...
const Navigation = () => {
  const { t, i18n } = useTranslation()
  const dispatch = useDispatch()
  const language = useSelector((state: RootStateType) => state.language)

  useEffect(() => {
    dispatch(getLanguageBegin())
    dispatch(getAllAnimeBegin())
  }, [])

  useEffect(() => {
    i18n.changeLanguage(language)
  }, [language])

由於明天開始要進行設定頁面的改造,對於redux的相關操作就等明天一起處理

參考:redux: combineReducers


上一篇
[Day24] 語言切換-1:沒有萬能的許願機
下一篇
[Day26] 設定頁面-1:頁面改造
系列文
我不用Expo啦,React Native!33

尚未有邦友留言

立即登入留言