0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

xstateを使って、多段コンポーネント間でre-renderをなるべく少なく表示する方法

Last updated at Posted at 2021-07-21

問題

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は取れるけど、この場合statesendが変わってしまうので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])

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?