0
votes

In this simple example, I am memoizing Child component using useMemo and passing callback back to parent function to add data to react hook array, which is initially set as empty.

My question is as follows: Why hook data never changes and keeps it's initial state in callback function that comes from Child component which is memoized. However, when checking data hook in useEffect method or using that hook in rendering - it always has the newest state.

It is more of a general question what happens behind the hood and what is the best way, for example, to check against duplicate values in callback functions from memoized child components if data always has initial state.

I am aware of useReducer and that I can manipulate data in hooks setter method before adding it, but maybe there are other methods?

Parent component:

import React, {useEffect, useState} from 'react';
import Child from "./Child";

function App() {

const [data, setData] = useState([]);

useEffect(()=>{
    console.log("UseEffect", data)
},[data]);

return (
<div>
  <Child callback={(val)=>{
      console.log("Always returns initial state", data);  // <----------Why?
      setData(old=>{
          console.log("Return previous state", old);
            return [...old, val]
      })
  }} />

   Length: {data.length /*Always gets updated*/}
</div>
);
}

export default App;

Child component: In real scenario it is a map, that I want to render only once, without causing re-renders.

import React, {useMemo} from "react"

export default function Child({callback}) {
return useMemo(()=>{
    return <button onClick={()=>callback(1)}>
        Press!
    </button>
},[])
}
2
Why do you expect updated value before setData?Bhojendra Rauniyar
I don't expect updated value before setData, but it never changes even after multiple callbacks received, which means that setData was already called before, but hook didn't changeBenas.M
If I understand it correctly, you need to pass data as props and use it in your second argument of useMemo.Bhojendra Rauniyar
setData doesn't mutate original data array, it return a new array reference. you are not passing down new data reference, given you use memo. once you use memo, you have the first data reference, and it keeps that way forever.buzatto

2 Answers

3
votes

Is there a way to return new reference of callback, without adding dependencies in useMemo method

Like this

// returns *(always the same)* function that will forward the call to the latest passed `callback`
function useFunction(callback) {
  const ref = React.useRef();
  ref.current = callback;

  return React.useCallback(function() {
    const callback = ref.current;
    if (typeof callback === "function") {
      return callback.apply(this, arguments);
    }
  }, []);
}

and then:

export function Child({ callback }) {
  const handler = useFunction(callback);

  return useMemo(() => {
    return <button onClick={() => handler(1)}>Press!</button>;
  }, []);
}

or

function App() {
  const [data, setData] = useState([]);

  const callback = useFunction((val) => {
    console.log("Always returns the latest state", data);
    setData([...data, val]);
  });

  return (
    <div>
      <Child callback={callback} />

     Length: {data.length /*Always gets updated*/}
    </div>
  );
}

function Child({ callback }) {
  return useMemo(() => {
    return <button onClick={() => callback(1)}>
      Press!
      </button>
  }, [])
}
2
votes

As you said, your Child component is memoized, which means it will only update if one of its dependency changes. So at the first rendering, you create your Child component with the function you pass to the callback prop, and at this time, data is []. If you click on the button, data is updated correctly and so is the function passed to callback prop to Child, but since you did not set any dependency to useMemo, your Child component will not update and will still return its memoized version, with first callback prop it received, which indeed always log the initial value of data: [].

So all you need is to add callback to the list of dependencies of useMemo:

export function Child({ callback }) {
  return useMemo(() => {
    return <button onClick={() => callback(1)}>Press!</button>;
  }, [callback]);
}

This way, when the callback prop changes, your Child component will also update its onClick event handler.

Also, I recommend using eslint-plugin-react npm package, which will instantly warn you about missing dependencies in React hooks, and more generally about bad practices in your code.