React Native - Components 之間的溝通

在一個 React Native app 中,常常會需要讓不同階層之間的元件(components)彼此互相溝通 例如有一個 app 的架構如下:

而其溝通的模式大致可以分成:

  1. 父子元件的溝通
  2. 兄弟元件的溝通

Props

(example code: https://github.com/Lewis-Lo/Ainimal-Frontend-Props)

Props 是 Properties 的縮寫,簡單來說就是用來描述一個元件的屬性(ex.按鈕的顏色 字體大小等),props 由父元件傳給子元件,讓父元件能夠藉由傳入不同的 props 來控制子元件。

使用起來會長得像這樣:

<Component prop1={value1} prop2={value2} prop3={value3} />

Props vs State

State 也是用來表示一個元件的屬性,和 props 的不同之處在於 props 是唯獨的,且由父元件傳給子元件,子元件無法更動傳進來的 props。

至於 state 則是一個元件用來控制自己的屬性用的。例如要實做一個 Button,每當 Button 被按下一次就會把該 Button 上顯示的數字+1,就可以利用 state 來控制顯示出的數字。

example (參考:ButtonAddOnePanel.tsx)

// 定義出number這個state,使用setNumber來更改number
const [number, setNumber] = useState(0);

// 每當addOne這個function被呼叫時,會將number這個state加1
const addOne = () => {
  setNumber(number + 1);
}

Props - 父子元件溝通

父子元件之間的溝通大致上可以分為父對子和子對父兩種

  1. 父對子 (參考:ParentToChildPanel.tsx) 在使用 Props 之前,要先定義出子元件需要那些資料,以 Square.tsx 為例

    // 定義出此元件的Props需要哪些資料
    interface Props {
      /* 必須要給的Props */
      size: number;
      color: string;
    
      /* 可給可不給的Props */
      text?: string;
    }
    

    在父元件(ParentToChildPanel)使用前面的 Square

    <View style={styles.container}>
      <Square size={150} color="#CFC" text='Square1' />
      <Square size={200} color="#ACD" text='Square2' />
      <Square size={100} color="#DAA" text='Square3' />
    </View>
    

    得到的結果

    在 Ainimal 中,幾乎所有的互動視窗(Modal)都是用預先寫好的 systemModal 來完成的,在需要使用的地方根據需求傳入不同的 Props 就能夠實現出各種不同的 Modal

  2. 子對父 先前有提到 props 是由父元件單方向傳給子元件的資料,那麼子元件要怎麼傳遞訊息給父元件呢? 我們可以直接將父元件中傳一個 callback function 給子元件,子元件只要呼叫傳進來的 function 就能夠更改父元件的資料。

    舉例來說,有一個 component 叫做 ParentSquare,ParentSquare 有一個 state 叫做 number,紀錄目前的數字,且有兩個子元件都叫做 ChildSquare

    若想要實現每當我點擊其中一個 ChildSquare 時會將 ParentSquare 的 number 加 1,點擊另一個時會減 1。那麼我們便可以現在 parent 裡先寫好把 number 加 1 和減 1 的 function,再傳進 child 裡,child 只要呼叫傳進來的 function 就可以操作 parent 的資料。

    (參考:ChildToParentPanel.tsx) ParentSquare 會長這樣:

    const ParentSquare: React.FC<Props> = ({}) => {
      const [number, setNumber] = useState(0);
    
      // 定義出要當作Props傳給Child的callback function
      const addOne = () => setNumber(number + 1);
      const SubOne = () => setNumber(number - 1);
    
      return (
        <View style={styles.container}>
          {/* 使用一個Text,方便觀察number的變化 */}
          <Text>{number}</Text>
    	  {/* 直接將function當作Props傳給ChildSquare */}
    	  <ChildSquare func={addOne} text='addOne'/>
    	  <ChildSquare func={SubOne} text='subOne'/>
        </View>
      );
    }
    

    ChildSquare 會長這樣:

    interface Props {
      func: ()=>void;
      text: string;
    }
    
    const ChildSquare: React.FC<Props> = ({func, text}) => {
      return (
        <TouchableOpacity style={styles.container} onPress={() => func()}>
          <Text>{text}</Text>
        </TouchableOpacity>
      );
    }
    

Props - 兄弟元件溝通

要利用 Props 完成兄弟元件之間的溝通非常直觀,只要利用前面所說的父對子以及子對父即可,流程大概會長這樣: 因為兩個 child 之間並沒有直接的關係,所以必須透過中間的 Parent,但是透過這種方法,當 app 功能越來越多,結構越來越複雜時,若有兩個離得很遠的 component 需要溝通的話,Props 的傳遞也會變得非常複雜,難以維護。

例如有個 app 的架構如下: 當 child1 想要傳訊息給 child2 時就會變得非常麻煩。

此時便可以使用 Redux 來管理 app 的 state,Redux 能夠將 state 集中管理且能夠跟所有的 component 直接互動,所以前面的溝通流程就可以被大幅簡化


Redux

Redux 是一個用來管理 state 的 library,且可以實現 UI 和資料分離。

引入 Redux 的好處除了可以讓 Component 之間的溝通更加簡潔之外,也讓被 Redux 集中管理的 state 更加安全,因為在 Redux 的管理之下,這些 state 無法直接被 component 改動,component 必須透過預先寫好的規則(action)以及提供的參數(payload)操作 state,因此讓這些 state 不會任意修改。

Three Principles

  1. Single source of truth : Programmer 先決定在這個 app 中,有哪些 state 會交給 redux 管理,而這些 state 都會被儲存在一個叫做store的地方,確保每個元件拿到的 state 都是一樣的。
  2. State is read-only : store裡存放的 state 是不能被任意更動的,唯有透過發送action才能更動store裡的內容,確保資料不會被任意更改。
  3. Changes are made with pure functions : 在更動 store 裡的內容時,reducer會根據 action 的內容以及現在的 state 計算出新的 state 並存回 store 裡,而 reducer 實際上就是預先定義好的 pure function。 (state, action) => newState

Store

在 redux 中Store負責管理所有的 state,且所有的元件都可以和store所儲存的 state 互動。

舉例來說,現在有一棵樹,而這棵樹的store管理以下這些 state:

  • Name
  • Age
  • Height

必須先定義出 initial state:

  • Name : HobbyTree
  • Age : 10
  • Height : 200cm

Action

Action是我們定義出要如何修改store裡 state 的規則,對於一棵樹來說,我們可以定義出灌溉 施肥 修剪這幾個action,其中每個action都可以定義要吃那些參數(payload)。

我們可以將這棵樹的action定義出來:

  • irrigate(100ml-5000ml)
  • fertilize(10g-500g)
  • trim(1cm-10cm)
  • next_year()

另外,在使用action時要搭配用來和 redux 溝通用的 dispatch 函式才會正確更新store

dispatch(irrigate(150));

Reducer

Action是在描述我們現在要對這棵樹做甚麼事,而reducer則是當接收到action時,計算 state 的邏輯,且reducer只會根據現在的 state 和action傳進來的參數計算出新的 state。

例如:

  • 樹原本 200 公分
  • 幫樹澆水 200ml
  • 樹長高 2 公分
  • 樹變成 202 公分

這個情境,在 redux 裡就會像這樣:透過 dispatch 傳來了 irrigate(200ml)這個actionreducer接收到之後要將 Height+2 更新回 store。

我們可以將reducer定義成:

  • Action : irrigate(water) Reducer : 將 Height 更新為 Height+(water/100)
  • Action : fertilize(fertilizer) Reducer : 將 Height 更新為 Height+(fertilizer/10)
  • Action : trim(length) Reducer : 將 Height 更新為 Height-length
  • Action : next_year() Reducer : 將 Age 更新為 Age+1

Provider

Provider是用來劃分store的使用範圍,通常會把provider直接包在 app 最外面,讓整個 app 裡所有的元件都能用store裡的資料。

Redux 運作流程

install

npm i redux
npm i react-redux
npm i -D @types/react-redux

Redux 使用步驟

(example code: https://github.com/Lewis-Lo/Ainimal-Frontend-Redux)

延續前面的種樹 app,第一個頁面會提共 4 種操作,分別對應到 4 個 action,且可以看到樹現在的狀態,第二個頁面會從 store 中拿資料,用來驗證結果。

![](https://i.imgur.com/gPITdhO.png =300x550)

Step1 定義 type

// tree.type.tsx

/* 定義data的type */
export interface TreeModel {
  Name: string,
  Age: number,
  height: number,
}

/* 定義action的type */
export interface IrrigateActionI {
  readonly type: "IRRIGATE",
  payload: number,
}

先定義出要交給 redux 管理的資料,可以是變數,也可以是物件,接下來定義出 action 的 type 和 payload,payload 一樣可以是變數或是物件。

Step2 完成 action

// tree.action.tsx

import { IrrigateActionI } from "../types/tree.type"

export const IrrigateAction = (ml: number): IrrigateActionI => ({
  type: "IRRIGATE",
  payload: ml,
})

實際定義出這個 action 要吃那些參數。

Step3 建立 reducer

// tree.reducer.tsx

const initState:TreeModel = {
  Name: "tree",
  Age: 1,
  height: 100,
}

const reducer = (state = initState, action: TreeActionType): TreeModel => {
  let newState = {...state};

  switch (action.type) {
    case "IRRIGATE" :
      newState.height = state.height + action.payload/100;
      return newState;

    default :
      return state;
  }
};

創建一個新的 reducer,並將初始狀態以及此 reducer 相關的 action type 傳入,並完成各個 action 對應到的運算邏輯,並 return 新的 state。

Step4 建立 store 並導入前面寫的 reducer

// store.tsx

import { createStore, combineReducers } from "redux";
import treeReducer from './reducers/tree.reducer';

const rootReducer = combineReducers({
  treeReducer,
});

const store = createStore(
  rootReducer,
);

先用 combineReducers 將所有的的 reducer 合併成 rootReducer,並傳入 createStore 就可以建立 store。

Step5 將 Provider 包在 app 最外面

// App.tsx

<Provider store={store}>
  <SafeAreaProvider>
    <Navigation colorScheme={colorScheme} />
    <StatusBar />
  </SafeAreaProvider>
</Provider>

Step6 利用 useSelector 從 store 中拿取資料

// TabOneScreen.tsx

/* 一次把整個reducer的資料拿出來 */
const treeState = useSelector((state: storeTypes) => state.treeReducer);

/* 只拿reducer中的Age */
const age = useSelector((state: storeTypes) => state.treeReducer.Age);

![](https://i.imgur.com/LFcyZ00.gif =300x550)

可以看到,我們只是要讓 redux 幫忙管理幾項簡單的資料,但是要寫的 code 非常多,因此當 app 足夠複雜時才能凸顯出使用 redux 的好處。

Redux-Middleware

Redux middleware 指的是在 dispatch action 後,進到 reducer 前,這段時間先交由 middleware 進行額外處理,等到 middleware 執行完後,再交給 reducer 更新 store 裡的內容。

在原本的 redux 中,dispatch 的流程為 synchronously,當一個網頁或是 app 需要在 action 裡做一些非同步的事情(ex. api calls)時就會出問題。而 middleware 主要就是要解決這種狀況。

直接在 action 裡打 api:

export const IrrigateAction = async (ml: number): Promise<IrrigateActionI> => {
  const res = await axios.get("http://numbersapi.com/random/year");
  console.log(res);
  return({
    type: "IRRIGATE",
    payload: ml,
  })
}

會直接 error。

加入 middleware 後 redux 的運作流程

Middleware 實際上就是一個包裝了原本的 dispatch,讓 dispatch 可以做更多事情:

  • 讓 action 被丟出來時可以先做其他事情(ex.紀錄 action state, api calls, ...)再進到 reducer
  • 暫停,中斷,替換被 dispatch 出來的 action
  • 修改 action type, 移除,修改,新增 payload

讓 redux 可以完成更多任務,使用上也更加有彈性。

另外我們也可以將多個 middleware 串在一起使用:

Redux Thunk

Redux-thunk 是一個可以實現非同步 action 的 middleware 套件。 其原理是在當發現 dispatch 出來的東西是一個 function 而不是正常的 action object 時,就先執行 function 的內容,且不要進到 reducer,等到回傳出 action object 的時候再把這個 object 交給 reducer 計算新的 state。

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

其中 next 是用來把 action 傳給下一個 middleware 用的,如果沒有下一個 middleware 的話就會進到 reducer 裡。如此一來便可以將 action 的功能擴充

install

npm install redux-thunk --save

使用步驟

首先把 redux-thunk 加進store

// store.tsx
import { createStore, combineReducers, applyMiddleware } from "redux";
import treeReducer from './reducers/tree.reducer';
import thunk from "redux-thunk";

...

const store = createStore(
  rootReducer,
  applyMiddleware(thunk),
);

...

接下來定義出非同步的action,假設新的asyncAction會打一支 api,如果有成功才會送出真的 action object

// tree.action.tsx
export const AsyncAction = () => {
  return async (dispatch: Dispatch<NextYearActionI>) => {
    try {
      // 如果成功的話就送出action object
      const res = await axios.get("http://numbersapi.com/random/year")
      console.log(res.data);
      dispatch({
        type: "NEXTYEAR",
        payload: {},
      })
    } catch (error) {
      // 沒有成功的話就不送出action object
      console.log(error);
    };
  }
}

這樣就可以直接dispatch(asyncAction())

另外,也可以根據狀況丟出不同的 action object,例如:

export const AsyncAction = () => {
  return async (dispatch: Dispatch<NextYearActionI|TrimActionI>) => {
    try {
      // const res = await axios.get("http://numbersapi.com/random/year");
      const res = await axios.get("error");
      console.log(res.data);
      dispatch({
        type: "NEXTYEAR",
        payload: {},
      })
    } catch (error) {
      console.log(error);
      dispatch({
        type: "TRIM",
        payload: 5,
      })
    };
  }
}

這樣便可以成功達到前面所提到的攔截或是替換實際上要丟出去的action及其payload

AINIMAL人工社群智慧養成

找到與你最契合的人