在 React 提到 Global State 時,大家很常會想到 React context,沒錯我就是要使用它!
由於 React 跟 XState 都使用到 context 一詞,剛看文件時,在這裡很容易混淆。希望在這第 29 天,我的讀者們。已經對個詞有點概念了。
本篇範例程式碼( example code )皆出自 XState
簡單來說,我們就只要將 Machine Instance (或叫做 Service, Actor)存入 React Context 中,即可跨元件共享資料了!
import React, { createContext } from 'react';
import { useMachine } from '@xstate/react';
import { authMachine } from './authMachine';
export const GlobalStateContext = createContext({});
export const GlobalStateProvider = (props) => {
const authService = useMachine(authMachine);
return (
<GlobalStateContext.Provider value={{ authService }}>
{props.children}
</GlobalStateContext.Provider>
);
};
但我們知道 React context 的值一改變,底下所有 children 都會被重新渲染( rerender ),而 authService 一詞顧名思義,它並非 Primitive Type ( string
, number
, boolean
... ),我們在昨天提到 useMachine 是回傳一個 instance ,並且活在一個 Component 的生命週期裡,也就是說當我們的元件被 unmount 時,就會銷毀,當我們不斷 rerender ,就會一直建立新的 instance。這在 React context 下的使用會非常棘手,我們需要考慮可能會造成「效能不佳」的問題。
幸運的是,XState 提供我們另外一個 API - useInterpret
,它會回傳我們一個 service ,不過這個 serive 是一個靜態的 reference 。(也就是說,他不會隨著我們的生命週期而不斷銷毀、重建!)
export const GlobalStateProvider = (props) => {
- const authService = useMachine(authMachine);
+ const authService = useInterpret(authMachine);
return (
<GlobalStateContext.Provider value={{ authService }}>
{props.children}
</GlobalStateContext.Provider>
);
};
而底下要使用的人就是透過 useContext 取得 instance ,再透過 useActor 與它建立互動即可。
import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
import { useActor } from '@xstate/react';
export const SomeComponent = (props) => {
const globalServices = useContext(GlobalStateContext);
const [state] = useActor(globalServices.authService);
return state.matches('loggedIn') ? 'Logged In' : 'Logged Out';
};
而也正因為 useInterpret
, 回傳的是一個 static reference ,如果有需要的話,它也允許使用 observer 來訂閱這個 service 。可以監聽 狀態的變化 並透過 observer 執行對應的操作。
const App = () => {
const service = useInterpret(
someMachine,
extraOptions,
// 用 observer 訂閱 instance ,當狀態改變就執行 console.log
(state) => {
console.log(state);
}
);
// ...
};
很多時候,我們拿到的資料或狀態不能直接使用,我們很常需要進行一些運算拿到我們需要的資料(Derived data)
如 Redux 官方提供 selector,讓我們能把 store 裡的一些 data 先進行一些運算,再回傳需要的資料給我們。
假如資料沒改變,Selector 讓我們能限制資料的運算、進而降低重新渲擾的次數、也幫助我們避免這些不需要的重複計算。
以上述原本的例子,我們原本透過 useActor
拿到的 state "Object" ,其實我們在意的也就是指當前狀態是不是 'loggedIn'
而已,一樣的概念,此時我們便可以透過 XState 的 selector 來幫忙,降低我們重新渲染的次數。
import React, { useContext } from 'react';
import { GlobalStateContext } from './globalState';
- import { useActor } from '@xstate/react';
+ import { useSelector } from '@xstate/react';
+ const loggedInSelector = (state) => {
+ return state.matches('loggedIn');
+ };
+
export const SomeComponent = (props) => {
const globalServices = useContext(GlobalStateContext);
- const [state] = useActor(globalServices.authService);
+ const isLoggedIn = useSelector(globalServices.authService, loggedInSelector);
- return state.matches('loggedIn') ? 'Logged In' : 'Logged Out';
+ return isLoggedIn ? 'Logged In' : 'Logged Out';
};
這樣子,只有當 state.matches('loggedIn')
回傳不同的結果,才會使 Component 重新渲染。比起使用 useActor
,在這個情境下,透過 useSelector
是官方推薦的使用方式。
此時假設有發送 event 的需求怎麼辦?
還記得之前我們是透過 const [state, send] = useActor
拿到 send 方法來發送事件嗎?
我們在這裡可以透過解構 const { send } = globalServices.authService
,直接從 context 的 serice 拿出 send 方法來使用。或者直接執行 globalServices.authService.send('LOG_OUT')
。
明天會跟大家分享一點點 Redux 跟 XState 的比較 and 推薦一些學習資源讓大家能更進一步學習。
ya~~ 最後一天囉
學習愉快 ^^
https://xstate.js.org/docs/recipes/react.html
https://xstate.js.org/docs/packages/xstate-react/