iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Software Development

From State Machine to XState系列 第 28

Day 28 - XState in React (著重: local state)

前面介紹許多 State Machine 及 XState 的功能,由於篇幅不多了,今天想跟大家先快速的介紹一下在 React 中如何使用 XState。

快速小回顧一下我們之前學到的一些 API

本篇範例程式碼( example code )皆出自 XState

XState Review

1. createMachine 將定義(config)建立出 FSM

2. interpret 將 FSM 建立出 Instance、Service

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');

3. config 可以描述 階層式狀態

4. config 可以描述 平行式狀態

5. config 可以透過 context 共享資料

6. config 可以透過 Guard 保衛狀態轉移

7. config 可以透過 Action 執行 Side Effect


XState in React

1. useMachine API

useMachine 可以在這個 Component 的生命週期裡,將我們經過 createMachine 的一台 FSM 啟動成為一個服務。

可以從 useMachine 回傳的一組 Tuple ,拿到下面 3 個東西

  1. state 這個 service 當下的 State 物件,很常被命名為 current 或 currentState
    (其實也就是前面 Demo XState 時一樣的 State Object)
    state.value 拿到 當前 state;
    state.matches('stateName') 回傳 true / false 告訴我們當前是不是我們期待的 state
  2. send 派發事件
    send('eventName') 可以在該 service 發送事件
  3. service 這個 FSM 產生的 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!

2. useService or useActor

有時候,我們也會想將這個 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>
      );
    }

3. Side Effect in React x XState

如果我們想要在某個 transition 前後執行 side effect 時,前面我們知道要使用 action ,並在裡面訂定 side effect 要做什麼,但假如今天 我們的 side effect 是想要與 React 元件互動的話...

3.1 我的 action 要被當成 React 裡的 useEffect 執行 -> asEffect

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} />;
};

3.2 要執行的 side effect 是在 React 裡面的其他 hook function。

比如點了某個 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;
};

3.3 透過 useEffect 將 XState 狀態機 與 React 的資料更新同步

有時我們進行 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]);
};

4. 動態 JSX 的 UI 展現

也可以透過 switch / caseif / 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/


上一篇
Day27 - 子狀態 or 子狀態機?與外部溝通!概念簡介: invoke services v.s. spawn actors in XState
下一篇
Day 29 - XState in React 2 (著重: global state and performance concerned)
系列文
From State Machine to XState31

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-14 12:08:22

恭喜即將邁入完賽啦~

Ken Chen iT邦新手 5 級 ‧ 2021-10-14 19:07:27 檢舉

/images/emoticon/emoticon01.gif

我要留言

立即登入留言