The problem with the following snippet is that components A
and B
will be rerendered whenever the change button is clicked, whereas I would like only component C
to be rerendered as it is the only one consuming context:
const UserContext = React.createContext({user: {name: 'name1'}, change: () => null})
const useUserContext = () => React.useContext(UserContext)
const A = () => {
console.log('rendering A')
return <B/>
}
const B = () => {
console.log('rendering B')
return <C/>
}
const C = () => {
const user = useUserContext()
console.log('rendering C', user)
return (
<div>
<div>UserName:{user.user.name}</div>
<button onClick={user.change}>Change</button>
</div>
)
}
const App = () => {
const [user, setUser] = React.useState({name: 'name2'})
const change = () => {
console.log('changing name')
setUser({name: 'name3'})
}
return (
<div>
<UserContext.Provider value={{user, change}}>
<A/>
</UserContext.Provider>
</div>
)
}
I mitigated the issue completely by extracting the provider logic to a custom provider:
const UserContext = React.createContext({user: {name: 'name1'}, change: () => null})
const useUserContext = () => React.useContext(UserContext)
const UserProvider = ({children}) => {
const [user, setUser] = React.useState({name: 'name2'})
const change = () => {
console.log('changing name')
setUser({name: 'name3'})
}
return (
<UserContext.Provider value={{user, change}}>
{children}
</UserContext.Provider>
)
}
const A = () => {
console.log('rendering A')
return <B/>
}
const B = () => {
console.log('rendering B')
return <C/>
}
const C = () => {
const user = useUserContext()
console.log('rendering C', user)
return (
<div>
<div>UserName:{user.user.name}</div>
<button onClick={user.change}>Change</button>
</div>
)
}
const App = () => {
return (
<div>
<UserProvider>
<A/>
</UserProvider>
</div>
)
}
However, I don't understand why this should make any difference to React? Can someone explain what happens behind the scenes in both cases?
JSX
compiles into React.createElement(...)
and it returns an object. Diffing algorithm compares that object on each render. If UserProvider
accepts children
prop as component instance (an object from createElement
), that object does not change its reference even if UserProvider
rerenders, since it is not created inside UserProvider
, but in its parent.
In your first example React.createElement(A)
gets called on each render. In your second example - only once in App
If you transform your first example like this:
const children = <A /> // createElement is called only once, outside of the component
const App = () => {
const [user, setUser] = React.useState({name: 'name2'})
const change = () => {
console.log('changing name')
setUser({name: 'name3'})
}
return (
<div>
<UserContext.Provider value={{user, change}}>
{children}
</UserContext.Provider>
</div>
)
}
It will not rerender