問題
const [state, send] = useMachine(machine)
こうしてしまうと、send("EVENT")
が起きる度にstate.value
が変わったり、state.context
が変わったりするので、re-renderが起きてしまう。
極局所的な箇所であればre-renderはさほど気にならないが、コンポーネントが多段で各コンポーネントの描画が重いとかになるとre-renderがとてももっさりして最終的には使い物にならなくなったりする。
解決
- useInterpreter
- useSelector
- ReactのContextかJotai
を使う。
useInterpreter
const service = useInterpret(someMachine);
serviceが返ってきて、これは不変。service.send("EVENT")
してもservice
自体のオブジェクトは変わらないので、re-renderが起きない。
const [state, send, service] = useMachine(someMachine);
としてもservice
は取れるけど、この場合state
、send
が変わってしまうのでre-renderが起きてしまう。
useSelector
不変のserviceでre-renderが起きなくなったとすると、全く画面更新が起きないので、それはそれで困る。そこでuseSelector。
useSelectorを使うと特定のstateに対して反応して、renderを起こしてくれる。
const updated = useSelector(service, (state) => state.matches("updated"))
これによって、子コンポーネントとかで特定のstateだけをwatchする形にすれば、不要な再描画が全く起きなくなる。
ReactのContextかJotai
多段コンポーネント間でuseInterpretで生成したserviceを使わなければならない。
// これは毎度呼ぶ度に新しいserviceを作ってしまうので、
// 親子で個別に呼んでしまうとそれぞれのserviceオブジェクトになってしまうので意味がない。
const service = useInterpret(someMachine);
回避策1(ダメな例)
import { useInterpret } from "@xstate/react"
import React from "react"
import machine, { Service } from "./machine"
export const useService = () => {
const [service] = React.useState<Service>(useInterpret(machine))
return service
}
React.useStateで管理しようとしても、どうもuseInterpret側の実装なのかuseSelector
の動きがおかしい。
回避策2
React.Contextを使う。単純に親コンポーネントでuseInterpretからserviceを生成して、contextに保持しておく。子コンポーネントではそれを引き出して使う。
// 親コンポーネント
export const ServiceContext = React.createContext({} as { service: Service })
...
...
const service = useInterpret(machine)
return (
<ServiceContext.Provider value={{ service }}>
{props.children}
</ServiceContext.Provider>
)
...
...
// 子コンポーネント
...
const {service} = React.useContext(ServiceContext)
// .matches とか .contextとか見てbooleanを返す
const isSomeState = useSelector(service, (state) => state.matches("someState"))
...
回避策3
Jotaiを使う。基本的にReact.Contextと同じようなこと。initialValuesとして親コンポーネントでserviceを渡しておいて、それを子コンポーネントで使う形。
// 親コンポーネント
import { Atom, Provider } from "jotai"
const serviceAtom = atom<Service, Service>(null, (get, set, update) => {
set(serviceAtom, update)
})
...
const element: [Atom<Service>, Service] = [serviceAtom, useInterpret(machine)]
return (
<Provider initialValues={[element]}>
...
</Provider>
)
// 子コンポーネント
import { useAtomValue } from "jotai/utils"
...
// 不変だし変更する必要がないので、useAtomValueで値だけを取る
const service = useAtomValue(serviceAtom)
// .matches とか .contextとか見てbooleanを返す
const isSomeState = useSelector(service, (state) => state.matches("someState"))
machineからServiceのtypeを作る方法
ActorRefFrom
を使って対象のmachineをtypeofで渡す
とService typeが作れるっぽい。
import { ActorRefFrom } from "xstate"
import machine from "./machine"
type Service = ActorRefFrom<typeof machine>
全体的にstateの状態で反応したい場合
useEffect(() => {
const subscription = service.subscribe((state) => {
console.log(state)
console.log(state.context)
})
return subscription.unsubscribe;
}, [service])
参考