Pure Functional Components : Memoize + Callback + Reference => MemoizedCallback

Kaushik NP
4 min readJul 22, 2020

--

You have heard how Functional Components are the future, and have heard the whispers about how React will in the future inherently provide optimisations to such components. So you make up your mind about using only Functional Components, and it sure feels better. The journey till now is easy and breezy, utilising useState, those little devils :), breaking down components to its purest forms, without touching upon states unnecessarily and you go about your life. But then you decide that the app feels a bit slow and its time that maybe you begin with optimisations.

Having heard about Pure Components, it’s clear that that’s the way to go about it. And of course, React let’s you replicate the same with memo. Now you have a way to implement Pure Component in Functional Components. So you go ahead and memoize the component. And theoretically, it should work. You are not making any changes to the state, not setting view based on inputs, and hence should not be a cause for any re-renders. If only we lived in such a perfect world.

The Issue

Generally, states are handled by a main component which holds multiple child components. And here, the child components only render its view, without changing the state, which is in fact accomplished by passing a function to the child from its parent component. When called, the state gets updated, and though it does not affect your child component, it seems to be triggering a render of the child component too along with its parent component.

Let’s take a simple example. We have a Parent Component maintaining count using state, which gets incremented in the Child Component.

The Parent Component looks something like below

export default function ParentComponent() {
const [touchCount, setTouchCount] = useState(0);
const onPress = () => setTouchCount(touchCount + 1);
return (
<>
<ChildComponent onPress={onPress} />
<Text>{touchCount}</Text>
</>
);
}

The Parent stores the `count` while it also passes the function to increment the count `onPress`.

Meanwhile, the Child Component just calls the `onPress` function. Note that we have wrapped the component inside Memo, and it's not handling any state changes.

export default React.memo(({ onPress }) => (
<TouchableOpacity
onPress={() => onPress()}
/>
));

Profiling this shows us exactly what’s going on behind the scenes.

Flamegraph without a true Memoized Component
Profiling without a true Memoized Component

Notice the Child Component re-rendering with every frame. This creates an extra overhead even though the component has not really changed and does not necessitate for a re-render. So, `memo` did not do what we expected. Let’s look at how we can improve upon this.

The Solution

Memoize + Callback + Reference => MemoizedCallback

The issue is that every render of the parent component creates a new function reference, which then triggers a re-render of the child component. What that would mean is that even if the component has not undergone any change, the parent component’s rendering causes a re-render of the child component. That is not separation of cause, and something that can come back to bite us in the a** later on. The solution to this is to Memoize your Callback function such that it’s reference remains the same across multiple renders.

Now, the question would be why include Reference? Because using just Callback would fail, and result in a similar situation as previously seen. Ref here would help as mutating the .current property doesn’t cause a re-render. Meaning, we can re-memoize when any of the dependency changes without triggering a render.

Now let’s define our MemoizedCallback hook.

export default (callback, inputs = []) => {
// store the callback
const callbackRef = useRef(callback);
// ref callback
const memoizedCallback = useCallback(
(...args) => (0, callbackRef.current)(...args), []
);
// update callback function depending on inputs
useEffect(() => {
callbackRef.current = callback;
}, inputs);
return memoizedCallback;
};

Change the `onPress` function to use the MemoizedCallback.

const onPress = useMemoizedCallback(
() => setTouchCount(touchCount + 1), [touchCount]
);

Looking at the Flamegraph with the Memoized Callback, we notice the difference almost immediately. With rendering happening now within a millisecond. Notice the _c (Memo) element is greyed out, signifying that it was never rendered.

Flamegraph with the MemoizedCallback
Profiling with optimised solution

And the effect of this is felt heavily across ‘pure’ functional components which take a significant amount of time to render though they do not handle data internally. And this cost can be cut down without much change required.

This certianly has the capacity to make the difference between a butter smooth app and an almost 60 fps app!

Reference materials / Inspirations

--

--

Kaushik NP
Kaushik NP

Written by Kaushik NP

CoFounder @Kaamik | Exploring Future Tech @ Dubai

No responses yet