iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 9
0
Modern Web

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

[Day9] Redux:我是大家的無人機

今日關鍵字:無人...Redux


假如有一天,你在家煮了咖哩
不小心煮了太多
想分給對面的人
如果是以前都是平房時
https://ithelp.ithome.com.tw/upload/images/20200909/20121828D5Mx4WmRnj.png
直接走過去就好

如果場景變換成現代
你住在高樓裡該怎麼辦呢(用丟的?)
https://ithelp.ithome.com.tw/upload/images/20200909/20121828lMBLugoHu1.png
正常要先端著咖哩走到一樓,走到對面那棟後再爬上去...
通常做過一次之後應該不會想再做第二次了/images/emoticon/emoticon03.gif
然後更火大的是總算爬到了對面時,對面的人說:阿樓上的人剛剛分我咖哩了

這個煩惱直到有一天,管委會購買了一套無人機管理系統
透過特定的指令像無人機發送命令(可以新增)
可以讓無人機幫你運送咖哩到對面
https://ithelp.ithome.com.tw/upload/images/20200909/201218283s0bkBx5KL.png
而且由於是通過系統命令,所以下過的指令系統都會有紀錄
只要拿起家中管理系統的平板一看就知道
不會再有資訊未更新的問題


當網頁和App不大時,資料還可以一層一層傳遞
但如果結構過於複雜時,這種傳遞法不僅麻煩還容易出錯
所以這時我們需要一個掌管全局狀態的機制
而React中使用的便是Redux

yarn add redux react-redux @types/react-redux

Redux給予了以下優點

狀態的可預測性

狀態的改變皆由action發出後經過reducer處理,不會發生props傳遞時不小心被改變,而導致最終難以預測狀態會如何變化的情形

元件的封裝

已經寫好跟store互動的邏輯,不用自己撰寫

渲染的優化

當store中的狀態真的發生改變時才會重新渲染,減少不必要的re-render


Redux的組成可分為以下幾個

Action

創造特定的指令,用於向reducer指定進行特定的動作,可以夾帶資料(咖哩)

Reducer

接收到action後進行資料的處理並更新狀態,這裡要記得reducer必須是pure function

Store

統整reducer

Provider

劃定store的使用範圍,在Provider內部才可以取用store的資料

當然視需要甚至能做到狀態統一由redux處理,component只負責將接收的資料呈現在畫面上
不過如果只是不需共用的狀態,其實可以不必使用到redux
(當然也可以使用react內建的context API,使用方法跟解決的問題是差不多的)
可以看看Dan寫的這篇:You Might Not Need Redux


在redux中處理非同步請求不太容易,通長是交給middeware處理
這裡使用的是redux-saga

yarn add redux-saga @babel/polyfill

由於這裡比較繁瑣,但沒有篇幅讓我講太多
秉持著原來的規劃邊做邊講吧

Reducer

import * as animeActions from '../action/animeAction'
import { Anime } from '../../data/content'

const initState: { allAnime: Array<Anime> } = { allAnime:[]}

const animeReducer = (state = initState, action: animeActions.AnimeAction) => {
  switch (action.type) {
    case animeActions.GET_ALL_ANIMATE_SUCCESS:
      return { allAnime: action.payload?.allAnime }
    default:
      return state
  }
}

export default animeReducer

這邊先規劃store中的狀態形式以及資料的處理

action

export const GET_ALL_ANIMATE_BEGIN = 'GET_ALL_ANIMATE_BEGIN'
export const getAllAnimeBegin = () => ({
  type: GET_ALL_ANIMATE_BEGIN
})

export const GET_ALL_ANIMATE_SUCCESS = 'GET_ALL_ANIMATE_SUCCESS'

export const getAllAnimateSuccess = (allAnime: Array<Anime>) => ({
  type: GET_ALL_ANIMATE_SUCCESS,
  payload: {
    allAnime
  }
})

創建兩個指令,一個是開始獲取資料,另一個是獲取資料成功後傳遞給reducer
redux-saga的角色就是component和reducer的中介,當資料獲取完成後才將指令連同資料一併送出
而不是資料還沒回來指令先送

// action
import { call, put, takeEvery } from 'redux-saga/effects'
...
function* getAnime() {
  const contentPromise = new Promise<Array<Anime>>((resolve, _reject) => {
    setTimeout(()=>{
      const allAnime=[Anime1 , Anime2.....]
      resolve(allAnime)
    },0)
  })

  const data = yield call(() => contentPromise.then((result) => result))
  // 在generator function(*)內yield可以在先停在這,配合call執行非同步操作等到拿到資料才繼續跑

  yield put(getAllAnimateSuccess(data))
  // 使用put觸發下一個action,將取完的資料送入reducer
}

function* animeSaga() {
  yield takeEvery(GET_ALL_ANIMATE_BEGIN, getAnime)
}
 // takeEvery使得接到GET_ALL_ANIMATE_BEGIN時觸發getAnime

export default animeSaga

animeSaga()這個generator function便是在訂閱事件

saga

import { all } from 'redux-saga/effects'
import animeSaga from '../action/animeAction'

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

export default rootSaga

用all來整合訂閱事件的generator function

store

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { composeWithDevTools } from 'redux-devtools-extension'

import animeReducer from '../reducer/animeReducer'
import rootSaga from '../sagas'

// 創建middleware
const sagaMiddleware = createSagaMiddleware()

// 創建store,並且加入reducer及middleware
const store = createStore(
  animeReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware))
)
// 執行訂閱事件的saga
sagaMiddleware.run(rootSaga)

export default store

這裡偷偷多安裝了Redux DevTools Extension

yarn add redux-devtools-extension

使用方法如上,直接包在applyMiddleware()外就可以了
作用跟redux-logger類似,同樣是debug的工具(redux-logger有點像在洗console所以我個人不怎麼喜歡用)
跟web開發不同的是,Redux DevTools Extension無法直接在瀏覽器使用
必須安裝react-native-debugger
介面長這樣
https://ithelp.ithome.com.tw/upload/images/20200909/20121828LyajAmAE5K.png
(跟瀏覽器操作感覺頗像的)

store建置好後
打開App.tsx,以Provider包住Navigation

import React from 'react'
import { Provider } from 'react-redux'

import store from './src/redux/store/store'
import Navigation from './src/components/Navigation'

const App = () => (
  <Provider store={store}>
    <Navigation />
  </Provider>
)

export default App

這樣整個App內到處都可以自由取用store

然後HomeScreen內的資料便能從store取得
這裡需要兩個redux的hook
useDispatch為發號指令用的hook,使用時別忘記import對應的action
useSelector為接收狀態用的hook

// HomeScreen.tsx
import { useDispatch, useSelector } from 'react-redux'
import { getAllAnimeBegin } from '../redux/action/animeAction'

const HomeScreen = () => {
  const dispatch = useDispatch()
  const allAnime = useSelector((state: RootStateType) => state.allAnime)
  ...
  useEffect(() => {
    dispatch(getAllAnimeBegin())
  }, [])
  
  // 這裡沒改變
  const playListByWeek = useMemo(() => {
    .....
    allAnime.forEach((anime) => {
    .....
  }, [allAnime])
  
  ...
}

useEffect

react的function component內用來模擬class component生命週期的hook
當第二參數的陣列中的依賴量發生變化時,便會執行第一參數的函式
如果第二參數為空陣列時第一參數僅會在component生成時執行一次


今天幹的事情是原本寫在component中的變數(嗯...連狀態都不是)整個挪到store裡
這樣以後別的畫面需要用到全部動畫資料的陣列時也能方便地取用
不用傳遞props傳遞到吐血

明天預計新增動畫的細節頁並建立起連結

參考:


上一篇
[Day8] 輪播:神奇的上下交錯
下一篇
[Day10] 建立與細節頁面之連結
系列文
我不用Expo啦,React Native!33

尚未有邦友留言

立即登入留言