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