前面介紹許多 State Machine 及 XState 的功能,由於篇幅不多了,今天想跟大家先快速的介紹一下在 React 中如何使用 XState。
快速小回顧一下我們之前學到的一些 API
本篇範例程式碼( example code )皆出自 XState
import { createMachine, interpret } from 'xstate';
const toggleMachineConfig = {
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
};
// createMachine API 把 config、definition 建立成一個 pure function 的狀態機
// 讓 interpreter 可以使用
const toggleMachine = createMachine(toggleMachineConfig);
// interpret API 把 state machine 建立成一個 instance、service 供現實應用
// 比如像監聽 狀態的轉移、記憶當前狀態
const toggleService = interpret(toggleMachine)
.onTransition((state) => console.log(state.value))
.start();
// => 'inactive'
// 比如像 發送事件
toggleService.send('TOGGLE');
// => 'active'
toggleService.send('TOGGLE');
useMachine 可以在這個 Component 的生命週期裡,將我們經過 createMachine 的一台 FSM 啟動成為一個服務。
可以從 useMachine 回傳的一組 Tuple ,拿到下面 3 個東西
state.value
拿到 當前 state;state.matches('stateName')
回傳 true
/ false
告訴我們當前是不是我們期待的 statesend('eventName')
可以在該 service 發送事件import { useMachine } from '@xstate/react';
import { toggleMachine } from '../path/to/toggleMachine';
function Toggle() {
const [state, send, service] = useMachine(toggleMachine, {...someExtraOptions});
// someExtraOptions 就像是我們之前學過得掛載 Actions 跟 Guards
return (
<button onClick={() => send('TOGGLE')}>
{state.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
useMachine 像是 useState 、 useEffect ,常被使用於 Component 的 Local State!
有時候,我們也會想將這個 Machine Instance 作為 props 傳給子元件使用,此時底下的子元件就需要透過 useService 及 useActor 來使用這個被建立好的 instance
import { useMachine } from '@xstate/react';
import { toggleMachine } from '../path/to/toggleMachine';
function ParentComponent() {
const [state, send, service] = useMachine(toggleMachine);
return (
<div>
<ChildComponent serivce={service} />
</div>
);
};
由於 Service 也是 Actor ,useService 的 API 將會在 V5 被棄用,原本透過 send event 額外帶的資料,方式也從 send('TOGGLE', extraData)
改為 send({ type: 'TOGGLE', ...extraData })
!
import { useActor, useService } from '@xstate/react';
function ChildComponent({service}) {
- const [state, send] = useService(service); // V5 將被棄用
+ const [state, send] = useActor(service);
+ const extraData = { x:0,y:1 };
return (
- <button onClick={() => send('TOGGLE', extraData)}>
+ <button onClick={() => send({ type: 'TOGGLE', ...extraData })}>
{state.matches('inactive') ? 'Off' : 'On'}
</button>
);
}
如果我們想要在某個 transition 前後執行 side effect 時,前面我們知道要使用 action ,並在裡面訂定 side effect 要做什麼,但假如今天 我們的 side effect 是想要與 React 元件互動的話...
XState 也提供 asEffect 這個 API,當 transition 發生時,action 不會馬上被執行,他會被作為 useEffect 裡的 side effect 被執行。
類似API 如 useLayoutEffect 也有對應的 asLayoutEffect
const machine = createMachine({
initial: 'focused',
states: {
focused: {
entry: 'focus'
}
}
});
const Input = () => {
const inputRef = useRef(null);
const [state, send] = useMachine(machine, {
actions: {
focus: asEffect((context, event) => {
inputRef.current && inputRef.current.focus();
})
}
});
return <input ref={inputRef} />;
};
比如點了某個 Toggle button 進入狀態轉換,轉換到下個狀態前,想要用 React Router 導向其他頁面。
我們先在 Machine Config 定義 action 的名稱 goToOtherPage
import { createMachine } from 'xstate';
export const machine = createMachine({
initial: 'toggledOff',
states: {
toggledOff: {
on: {
TOGGLE: 'toggledOn'
}
},
toggledOn: {
entry: ['goToOtherPage']
}
}
});
再將 goToOtherPage
的實作放在 React 元件裡
import { machine } from './machine';
import { useMachine } from '@xstate/react';
import { useHistory } from 'react-router';
const Component = () => {
const history = useHistory();
const [state, send] = useMachine(machine, {
actions: {
goToOtherPage: () => {
history.push('/other-page');
}
}
});
return null;
};
有時我們進行 AJAX 獲取資料,像使用 SWR, React-Query 等,會非同步向外部取得資料。
此時我們可以藉由 useEffect 當資料發生改變時,就打出一個 send Event ,將新的資料傳入 machine instance 裡。
const Component = () => {
const { data, error } = useSWR('/api/user', fetcher);
const [state, send] = useMachine(machine);
useEffect(() => {
send({
type: 'DATA_CHANGED',
data,
error
});
}, [data, error, send]);
};
也可以透過 switch / case
、if / else
、三元運算式state==='some'? <A /> : <B />
,搭配 XState 的 State Object
可以使用 state.value 或 state.matches('someState')
switch (state.value) {
case 'idle':
return (
<button onClick={() => send('FETCH', { query: 'something' })}>
Search for something
</button>
);
case 'loading':
return <div>Searching...</div>;
case 'success':
return <div>Success! Data: {state.context.data}</div>;
case 'failure':
return (
<>
<p>{state.context.error.message}</p>
<button onClick={() => send('RETRY')}>Retry</button>
</>
);
default:
return null;
}
https://xstate.js.org/docs/recipes/react.html
https://xstate.js.org/docs/packages/xstate-react/