Search code examples
javascriptreactjsreact-context

Why extracting React Context provider logic to a custom provider avoids unnecessary rerenders?


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?


Solution

  • 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.

    • Render happens when the component type is different than the previous render.
    • Re-render happens when the component instance is different than the previous render.

    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