When we use React Context, we often face the problem that when the Provider's value changes (e.g., when a component below it modifies a property in the value), all components that use that context are re-rendered, even if they do not depend on the changed property value. This can lead to unnecessary re-renders and negatively impact the performance of our application.
For this issue, this library provides a simple solution.
You can use this library to individually read and write each property in React context without worrying about triggering a full re-render of all related components under the context.
npm i react-atomic-context
yarn add react-atomic-context
pnpm install react-atomic-context
Only 3kB
size.
import React from 'react'
import { createAtomicContext, useAtomicContext } from 'react-atomic-context'
const AppContext = createAtomicContext({
foo: 'foo',
bar: 'bar',
})
const Foo = React.memo(function Foo() {
const { foo, setFoo, setBar } = useAtomicContext(AppContext)
console.log('foo rendered')
return (
<div>
<p> this is foo: {foo} </p>
<button
onClick={() => {
setFoo(`foo${Math.random().toString().slice(0, 5)}`)
}}
>
change foo
</button>
<button
onClick={() => {
setBar(`bar${Math.random().toString().slice(0, 5)}`)
}}
>
change bar
</button>
<hr />
<Bar />
</div>
)
})
const Bar = React.memo(function Bar() {
const { bar, setBar } = useAtomicContext(AppContext)
console.log('bar rendered')
return (
<div>
<p> this is bar: {bar} </p>
<button
onClick={() => {
setBar(`bar${Math.random().toString().slice(0, 5)}`)
}}
>
change bar
</button>
</div>
)
})
export default function App() {
const initState = React.useMemo(() => {
return {
foo: 'foo',
bar: 'bar',
}
}, [])
return (
<AppContext.Provider
value={initState}
onChange={({ key, value, oldValue }) => {
console.log(`${key} changed from ${oldValue} to ${value}`)
}}
>
<Foo />
</AppContext.Provider>
)
}
Overall, the usage of react-atomic-context
is similar to regular React context. First, you create a context. Then, you use its Provider to wrap the components you want to render. Within the component, you can read and modify the context value using the provided use hook.
-
createAtomicContext
-
This method is used to create a context, similar to
React.createContext
, but it requires an object as the initial value. eg:import { createAtomicContext } from 'react-atomic-context' // The initial value is required and can only be one object const AppContext = createAtomicContext({ foo: 'foo', bar: 'bar', }) export { AppContext }
-
The created context provides a
Provider
component, which wraps the components to be rendered. It must be provided with a prop namedvalue
, whose value is typically similar to the initial value and contains properties consistent with the initial value(can not contain any extra properties). Changing value ofvalue
prop won't take effect. eg:const App = () => { // Right way const initValue = React.useMemo(() => { return { foo: 'foooo', bar: 'barrr', } }, []) // Wrong! "initValue" can not change, the provider does not care about the overall change of the initial value const initValue = { foo: 'foooo', bar: 'barrr', } // Wrong! Because "baz" is not in AppContext initial value. const initValue = React.useMemo(() => { return { foo: 'foooo', bar: 'barrr', baz: 'bazzz', // baz is invalid here. } }, []) return ( <AppContext.Provider value={initValue}> <YourComponent /> </AppContext.Provider> ) }
-
The Provider component also provides an additional
onChange
prop that accepts a function, which is called whenever any property changes. eg:const App = () => { const initValue = React.useMemo(() => { return { foo: 'foooo', bar: 'barrr', } }, []) const handleChange = React.useCallback(({ key, value, oldValue }, methods) => { console.log(`${key} changed from ${oldValue} to ${value}`) console.log('getters and setters', methods) }, []) return ( <AppContext.Provider value={initValue} onChange={handleChange}> <YourComponent /> </AppContext.Provider> ) }
-
There is no "Consumer" component available. The lib only supports accessing the context through calling
useAtomicContext
.
-
-
useAtomicContext
-
This method is used to retrieve the current value from the context (provided by the nearest Provider's
value
prop). The value must be accessed using deconstructed syntax, for example:const { foo } = useAtomicContext(context)
. eg:import { useAtomicContext } from 'react-atomic-context' const MyComponent = () => { // Right way const { foo, bar } = useAtomicContext(AppContext) if (foo) { return <div>{foo}</div> } return <div>{bar}</div> // Wrong! const value = useAtomicContext(AppContext) if (value.foo) { return <div>{value.foo}</div> } return <div>{value.bar}</div> }
-
For each property, there are two additional methods provided. Such as, for the property
foo
, bothgetFoo
andsetFoo
methods are provided. ThesetFoo
method is the only way to update the value of thefoo
property, whilegetFoo
is used solely to obtain the latest value of thefoo
property(in most case, just usefoo
instead of callinggetFoo
). eg:const MyComponent = () => { const { foo, setFoo, getFoo } = useAtomicContext(AppContext) return ( <div onClick={() => { setFoo(newValue) }} > {foo} </div> ) }
-
Accessor methods(getters and setters) are reference-stable and will not change, so you can confidently ignore them in dependency arrays. eg:
const MyComponent = () => { const { bar, setFoo, getFoo } = useAtomicContext(AppContext) React.useEffect(() => { if (getFoo() !== bar) { setFoo(bar) } }, [bar]) // It is safe to omit getFoo and setFoo here. return <div>...</div> }
-
Specifically, this hook will return a method named
get
that returns the whole current value of the context (the returned value is read-only, like snapshot of state, for debugging purposes only). eg:const MyComponent = () => { const { get } = useAtomicContext(AppContext) return ( <div onClick={() => { console.log('current value of context is', get()) }} > inspect current value </div> ) }
-
import type {
AtomicContextGettersType,
AtomicContextSettersType,
AtomicContextMethodsType,
ProviderOnChangeType,
} from 'react-atomic-context'
const initValue = {
foo: 'foo',
bar: 123,
baz: false,
}
const context = createAtomicContext(initValue)
/**
* Getters = {
* getFoo: () => string
* getBar: () => number
* getBaz: () => boolean
* }
*/
type Getters = AtomicContextGettersType<typeof initValue>
/**
* Setters = {
* setFoo: (newValue: string) => void
* setBar: (newValue: number) => void
* setBaz: (newValue: boolean) => void
* }
*/
type Setters = AtomicContextSettersType<typeof initValue>
/**
* Setters = {
* setFoo: (newValue: string) => void
* setBaz: (newValue: boolean) => void
* }
*/
type Setters = AtomicContextSettersType<typeof initValue, 'foo' | 'baz'>
/**
* Methods = {
* setFoo: (newValue: string) => void
* setBar: (newValue: number) => void
* setBaz: (newValue: boolean) => void
* getFoo: () => string
* getBar: () => number
* getBaz: () => boolean
* }
*/
type Methods = AtomicContextMethodsType<typeof initValue>
/**
* type of "onChange" callback type passed to Provider.
*
* OnChange = (
* changeInfo:
* | { key: 'foo', value: string, oldValue: string }
* | { key: 'bar', value: number, oldValue: number }
* | { key: 'baz', value: boolean, oldValue: boolean }
* methods: {
* getFoo: () => string,
* setFoo: (v: string) => void,
* getBar: () => number,
* setBar: (v: number) => void,
* getBaz: () => boolean,
* setBaz: (v: boolean) => void,
* get: () => { foo: string; bar: number; baz: boolean }
* }
* ) => void
*/
type OnChange = ProviderOnChangeType<typeof initValue>
-
Why provide
getFoo
when we can directly access the value offoo
?In most cases, there is no need to use
getFoo
. Accessingfoo
directly through destructuring informs React that the current component's rendering depends on the value offoo
. Thus, whenfoo
changes, the current component will be re-rendered. However, there are situations where we only want to retrieve the value offoo
without caring about its changes. In such cases, you should use thegetFoo
method. -
Why do I have to keep the Provider's
value
prop reference-stable?To ensure that the context of data changes has a stable single way, that is, by calling the setter method corresponding to the property. This also explains why the value passed to the provider cannot contain additional properties relative to the initial value when creating the context.
-
Why must context data be accessed in a deconstructed manner?
Virtually every access to a context property is preceded by a call to
useContext
, so the property access order must be kept steady and comply with react hook requirements. And it's always a good programming practice to declare the properties and methods to be accessed in a deconstructed way at the beginning, which is clearer and easier to maintain your component. -
Why my component still get rerendered?
Important point but easy to ignore is that do not forget to use
React.memo
to wrap your component. By the way, There are a fews reasons leading to react component rerender. Such as calling setState, using normal react context, changing the key of the component, etc.
Thank you very much and hope to raise any questions.