Update a callback function when it’s memoized in React

Ats
3 min readJan 7, 2024

--

I was struggling with memoized components in React. I wouldn’t say this is the best practice but I’ll note and share my findings here.

Photo by Lucrezia Carnelos on Unsplash

Background

While working with a component provided by a package, I struggled with implementing it as I expected. I struggled because it’s memoized in the package and never updated when I change the props. The following component is a simple example of what I was working with.

type ChildProp = {
text: string;
callback: () => void;
}

const arePropsEqual = (oldProps: ChildProp, newProps: ChildProp) => {
return oldProps.text === newProps.text
}

const Child = memo(({ callback, text }: ChildProp) => {
return (
<div>
<div>{text}</div>
<button onClick={callback}>Child Button</button>
</div>
)
}, arePropsEqual)

The provided component accepts a few primitive types and a few callback functions as the props. Then the component is memoized as checking the changes in the primitive types and it’s never updated even if I change the callback functions. In my case, I wanted to update a value assigned to a variable in the callback function depending on an event listener. The following function is a simple example of what I was working with.

const myRef = useRef(null);
const [text, setText] = useState<string>('initial state')

const onEvent = (message: string) => setText(message);
useEffect(() => {
myRef.current.addEventListener('exampleEvent', onEvent);
return () => {
myRef.current.removeEventListener('exampleEvent', onEvent);
};
}, []);

const propFunction = useCallback(() => {
console.log(text);
}, [text])

I think the cache problem is quite common in React. But the component is memoized inside the 3rd-party package and it’s not straightforward to customize the cache logic. Then I was looking for a way to update the variable in the callback function without changing the cache logic.

What I did

First of all, I was trying to update the variable in the callback function using a state like above. But it didn’t work as I expected. I guess the state in the callback function would be fixed when it’s passed to a child component (I need to investigate more).

When I created a sample code snippet and tested it, the value of state was never updated if the component was memoized.

import { useState, useCallback, memo } from 'react';

const Parent = () => {
const [text, setText] = useState<string>('initial state')

const setClicked = useCallback(() => {
setText('Clicked')
}, [setText])

const propFunction = useCallback(() => {
console.log(text); // => it always logs "initial state" even though the 'setClicked' is called
}, [text])

return (
<>
<div>This is the test: {count}</div>
<button onClick={setClicked}>Parent Button</button>
<Child callback={propFunction} text='Prop passed by parent'/>
</>
)
}

type ChildProp = {
text: string;
callback: () => void;
}
const arePropsEqual = (oldProps: ChildProp, newProps: ChildProp) => {
return oldProps.text === newProps.text
}

const Child = memo(({ callback, text }: ChildProp) => {
return (
<div>
<div>{text}</div>
<button onClick={callback}>Child Button</button>
</div>
)
}, arePropsEqual)

const App = () => {
return <Parent />
};

export default App;

Even though I clicked the Parent Button , the Child Button logs initial state in my console. The state seems a snapshot judging from the experiment.

Then I tested with ref instead of state like below. It worked as I expected.

import { useState, useCallback, useRef, memo } from 'react';

const Parent = () => {
const textRef = useRef<string>('initial ref')

const setClicked = useCallback(() => {
textRef.current = 'Clicked'
}, [])

const propFunction = useCallback(() => {
console.log(text); // => it logs "initial state" and "Clicked"
}, [text])

return (
<>
<div>This is the test: {count}</div>
<button onClick={setClicked}>Parent Button</button>
<Child callback={propFunction} text='Prop passed by parent'/>
</>
)
}

type ChildProp = {
text: string;
callback: () => void;
}
const arePropsEqual = (oldProps: ChildProp, newProps: ChildProp) => {
return oldProps.text === newProps.text
}

const Child = memo(({ callback, text }: ChildProp) => {
return (
<div>
<div>{text}</div>
<button onClick={callback}>Child Button</button>
</div>
)
}, arePropsEqual)

const App = () => {
return <Parent />
};

export default App;

Based on the experiment, the ref points the reference like a pointer in C++.

I wouldn’t say this is a good implementation because the content of the callback isn’t immutable, which means it could be changed somewhere unintentionally. It should essentially be solved differently like the provided component is recreated by myself. But I thought the difference is interesting and good to know to avoid unintended behavior.

That’s it!

--

--

Ats
Ats

Written by Ats

I like building something tangible like touch, gesture, and voice. Ruby on Rails / React Native / Yocto / Raspberry Pi / Interaction Design / CIID IDP alumni

No responses yet