How to Minimized React Children Function Component Render
Posted on: 2022-11-16
Introduction of React Children
If you have a hierarchy of components, you may have one or more components that wrap HTML elements around something else. For example, you can have a Layout
component that wraps a child component. The child can be anything from now to the future- it does not matter.
React uses the term children to inject another component inside your component. So, in the following example, we have a basic Layout
component with a single property, children
.
function Layout(props: { children: ReactNode }) {
return (
<div>
<h1>Layout</h1>
<main>{props.children}</main>
</div>
);
}
The component wraps any other component. For example, the following React App
component render a div
that has the Layout
component, and between <Layout>
and </Layout>
is what is injected inside the component.
export default function App() {
return (
<div className="App">
<Layout>
<div>Will print inside the main tag of the Layout component</div>
</Layout>
</div>
);
}
The result is an HTML output with a div
defined under the App
into the Layout
component.
<div className="App">
<div>
<h1>Layout</h1>
<main>
<div>Will print inside the main tag of the Layout component</div>
</main>
</div>
</div>
React's context can leverage the capability of children
to create a provider. I have discussed the pattern in a previous article that explains two patterns with React context. In short, it provides a clean context with a state encapsulated into a component that does not mingle with the implementation of the context.
React Children as a function A.K.A. Render Props
Before diving into the main problem, a React's children can return a function instead of a ReactNode
. The pattern is called Render Props by React. Returning a function opens the door and still renders a child component that is the return of the function but, at the same time, passes information of function to be called by any component down the hierarchy.
While you can use a hook to access functions or values down the hierarchy, having a function gives some flexibility by providing values or callback to any component under the component utilizing the render props pattern. For instance, if you are using a child component that cannot call your hook to access the value of your context, you can use the render props pattern to pass the information by the property. The hook restriction is a scenario if you do not develop both components.
The Problem
The trap with React props is the number of renderings. Here is a small example that shows that if a parent React component passes a function down that the whole tree of componenst is rendered when the parent change.
If you open the Console of the CodeSandbox you see many renders.
// Initial render (mounting)
App Render
UserContextProvider Render
Layout Render
SmallChild Render
SmallChildUsingContext Render
// Click the Layout button causes 4 renders
UserContextProvider Render
Layout Render // We do not need to re-render Layout
SmallChild Render // We do not need to render the SmallChild
SmallChildUsingContext Render
The Profiling
The screenshot below shows the React's dev tool that tells why the UserContextProvider
rendered: because a hook changed. The Layout
cause is of a property changed cause. The SmallChild
is because the parent changed and the SmallChildrenUsingContext
is because the context changed.
This last one is reasonable since the context has changed, same for the UserContextProvider
but the Layout
should not have!
The Explanation
The React documentation specifies:
... because the shallow prop comparison will always return false for new props, and each render in this case will generate a new value for the render prop.
The documentation provides a workaround which is to create the function outside the rendering. A quick modification by memoizing the children inside the UserContextProvider
limites the rendering to two components: UserContextProvider
and SmallChildUsingContext
. The UserContextProvider
is expected as its state changes, and the SmallChildUsingContext
must re-render since it uses a value that changes from the context.
The gain is that Layout
is not changing, which makes sense since it does not rely on any state of the context.
The Solution
The solution needs to be clarified for many. The following lines:
const fn = useMemo(() => props.children(inc), [props, inc]);
return <UserContext.Provider value={value}>{fn}</UserContext.Provider>;
Cannot be:
const fn = useCallback(() => props.children(inc), [props, inc]);
return <UserContext.Provider value={value}>{fn()}</UserContext.Provider>;
The reason is that the function passed down is the same but not the output of the function. Hence, only useMemo
is memoizing the children
property, in that example, the Layout
. The final output is now:
// Initial render (mounting)
App Render
UserContextProvider Render
Layout Render
SmallChild Render
SmallChildUsingContext Render
// Click the Layout button causes 2 renders
UserContextProvider Render
SmallChildUsingContext Render