banner
innei

innei

写代码是因为爱,写到世界充满爱!
github
telegram
twitter

Experience of abstracting component state using Jotai

Jotai is an excellent React state management library based on the atomic model. It uses a bottom-up approach to build global React state management, constructs states by combining atoms, and optimizes rendering based on atomic dependencies. This solves the problem of additional re-rendering in React context and eliminates the need for memoization.

With Jotai, it is convenient to separate the top-level component's mashed-up state into multiple atoms, separating state from UI. It can efficiently manage state, and within the component, you can also get the state's getter/setter as needed, reducing a significant amount of re-rendering.

Use useAtom instead of useState#

The usage of Jotai atoms is straightforward. Generally, you only need to put the initialValue of useState into atom(), and then replace useState directly.

const isShowAtom = atom(true)
const Foo = () => {
  const [isShow, setIsShow] = useState(true)
  // change to
  const [isShow, setIsShow] = useAtom(isShowAtom)
}

In addition to this, Jotai also provides useAtomValue and useSetAtomValue for on-demand use. If you don't consume atomValue, you don't have to use useAtom and can directly use useSetAtomValue. The benefit of this is that it won't cause the component to re-render when atomValue changes. In the following example, only Bar will be updated when the parent component clicks the button, and no re-rendering will occur:

const isShowAtom = atom(true)
const Foo = () => {
  const setIsShow = useSetAtom(isShowAtom)
  
  return <>
   <Bar />
   <button onClick={() => setIsShow(true)}>Show</button>
  </>
}

const Bar = () => {
  const isShow = useAtomValue(isShowAtom)
  // ...
}

::: info

Sidebar: How can I avoid excessive re-rendering caused by useState if I don't use Jotai?

In the example above, we can use useState + useContext to replace it.


const IsShowContext = createContext(false)
const SetIsShowContext = createContext<SetStateAction<boolean>>(() => {})
const Foo = () => {
  const [isShow, setIsShow] = useState(false)
  return (
    <IsShowContext.Provider value={isShow}>
      <SetIsShowContext.Provider value={setIsShow}>
        <FooImpl />
      </SetIsShowContext.Provider>
    </IsShowContext.Provider>
  )
}
const FooImpl = () => {
  const setIsShow = useContext(SetIsShowContext)

  return (
    <>
      <Bar />
      <button onClick={() => setIsShow(true)}>Show</button>
    </>
  )
}

const Bar = () => {
  const isShow = useContext(IsShowContext)
  // ...
}

However, this approach of breaking the context into pieces for performance optimization is very difficult to maintain when there are many states.

If you don't want to use a state library like Jotai, you can try the useContextState implementation from foxact, which is similar to the above approach.

:::

Use useContext and atom to encapsulate global state within components#

After replacing useState with useAtom as mentioned above, the component's state is externalized, which means the component cannot be reused, and its state is shared whenever it is reused. We can use useContext and atom together to encapsulate the component's state back within the component.

Let's use the previous simple example:

const FooContext = createContext(null)
const Foo = () => {
  const contextValue = useRef({
    isShowAtom: atom(false),
  }).current
  return (
    <FooContext.Provider value={contextValue}>
      <FooImpl />
    </FooContext.Provider>
  )
}
const FooImpl = () => {
  const { isShowAtom } = useContext(FooContext)
  const setIsShow = useSetAtom(isShowAtom)

  return (
    <>
      <Bar />
      <button onClick={() => setIsShow(true)}>Show</button>
    </>
  )
}

const Bar = () => {
  const { isShowAtom } = useContext(FooContext)
  const isShow = useAtomValue(isShowAtom)
  // ...
}

By using a context, we cleverly encapsulate the state back into the component. We pass a contextValue from the top-level component, mount the atom inside the context, and the child component consumes the atom as needed. Since this context has no dependencies, you can freely use contextValue anywhere without worrying about re-rendering caused by changes in contextValue. In this case, contextValue will never change, thanks to Jotai. You can subscribe to the changes of the atom by getting it from contextValue and using useAtomValue to respond to the changes in the component.

Efficiently subscribe to atoms with selectAtom#

Jotai provides the selectAtom function, which can create a new readOnly atom based on the original atom, mainly used to implement selectors.

For primitive types, there is no need to use this function. However, for reference types such as objects, according to the immutability principle, if changes occur inside a large object, a new object will be created. Any changes inside the object will cause components subscribed to the entire atomValue to re-render.


const objectAtom = atom({
  type: 'object',
  value: {
    foo: 1
  }
})
const Foo = () => {
  const setter = useSetAtom(objectAtom)
  
  return <>
   <Bar />
   <button onClick={() => setter(prev => ({...prev, value: { foo:1 }}))}>Show</button>
  </>
}

const Bar = () => {
  const { type } = useAtomValue(objectAtom)
  // ...
}

In the example above, even though Bar only consumes type, when the value of objectAtom changes due to the button click in Foo, Bar will also change.


const objectAtom = atom({
  type: 'object',
  value: {
    foo: 1,
  },
})
const Foo = () => {
  const setter = useSetAtom(objectAtom)

  return (
    <>
      <Bar />
      <button
        onClick={() => setter((prev) => ({ ...prev, value: { foo: 1 } }))}
      >
        Show
      </button>
    </>
  )
}

const Bar = () => {
  const type = useAtomValue(
    selectAtom(
      objectAtom,
      useCallback((atomValue) => atomValue.type, []),  // Pay attention here
    ),
  )
  // ...
}

However, using selectAtom to extract the internal value will solve this problem. Note that you need to wrap the selector passed in with useCallback or extract this function outside the component to ensure that the function remains the same in the next re-render, otherwise, it will cause excessive re-rendering.


The above three points cover most scenarios. Give it a try!

This article is synchronized to xLog by Mix Space
The original link is https://innei.in/posts/programming/jotai-experience-with-component-state-abstraction


Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.