'How to pass a method into a child element if the method relies on state variables in React

I've been learning react over the last few days and for the most part it makes sense, however there is one thing that is stumping me.

If I have

  • A Parent element with some state variables and a callback method
  • A child element that takes a callback method as a prop
  • The callback method relies on some piece of state that is in the parent element
  • I don't want to re-create the view object every time any state changes

Every time I try to do this, it seems like the child element is calling some older version of the parent element (presumably the instance of the parent that actually created the child) instead of the current version.

I'm getting the feeling that what I want is just wrong on a fundamental level and isnt The React Way

The reason that I am trying to do this is that my main parent contains 17 divs, each of which represent a key on a musical instrument, and each of which contains at least 20-30 divs. The lowest div (of which there are at least a few hundred) has an onClick event that I want to modify the functionality of based on whether modifier keys are held down (shift, control etc).

Currently I have Raised the state of the shiftPressed to be on the single parent element then passed down the value of that into each child through props, however re-rendering hundreds of divs whenever a user pushes shift takes quite a while.

I've made a code sandbox to show the current problem sandbox

Sandbox code:

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function App() {
  //Our state holding data
  const [state, setState] = useState(false);

  //Our state holding the view
  const [view, setView] = useState(<div></div>);

  const printState = useRef(null);

  //Component did mount hook
  useEffect(() => {
    reGenerate();
  }, []);

  //state update hook
  useEffect(() => {
    printState.current();
  }, [state]);

  //function to flip the state
  const flipState = () => {
    setState(!state);
  };

  //The method that updates the view
  //(The idea being that I don't want to update the view on every state change)
  const reGenerate = () => {
    setView(
      <>
        <p>
          State: {state && "true"} {state || "false"}
        </p>
        <Child callback={printState} />
      </>
    );
  };

  //Method for validation
  printState.current = () => {
    console.log("Printed state: " + state);
  };
  
  return (
    <div className="App">
      <h1>Parent-child-prop-problem (prop-lem)</h1>
      <ol>
        <li>click "force regeneration"</li>
        <li>
          click "flip state" and the value of state after the flip will be
          printed in console, but it won't show up on the HTML element
        </li>
        <li>
          Click "print state (from child)" and observe that the console is
          printing the old version of the state
        </li>
      </ol>
      <button onClick={flipState}>Flip State</button>
      <button onClick={reGenerate}>Force Regeneration</button>
      {view}
    </div>
  );
}

function Child(props) {
  return (
    <div>
      <button onClick={props.callback.current}>Print State (from child)</button>
    </div>
  );
}


Solution 1:[1]

Taking a quick peek at your sandbox code and I see that you are storing JSX in state, which is anti-pattern and often leads to stale enclosures like you describe.

I don't want to re-create the view object every time any state changes

"Recreating" the view is a necessary step in rendering UI in React as a result of state or props updating. State should only ever store data and the UI should be rendered from the state. In other words, treat your UI like a function of state and props. Toggle the state state value and render the UI from state.

Example:

export default function App() {
  //Our state holding data
  const [state, setState] = useState(false);

  const printState = useRef(null);

  //state update hook
  useEffect(() => {
    printState.current();
  }, [state]);

  //function to flip the state
  const flipState = () => {
    setState(!state);
  };

  //Method for validation
  printState.current = () => {
    console.log("Printed state: " + state);
  };

  return (
    <div className="App">
      <h1>Parent-child-prop-problem (prop-lem)</h1>
      <ol>
        <li>
          click "flip state" and the value of state after the flip will be
          printed in console, but it won't show up on the HTML element
        </li>
        <li>
          Click "print state (from child)" and observe that the console is
          printing the old version of the state
        </li>
      </ol>
      <button onClick={flipState}>Flip State</button>
      <p>State: {state ? "true" : "false"}</p>
      <Child callback={printState} />
    </div>
  );
}

function Child(props) {
  return (
    <div>
      <button onClick={props.callback.current}>Print State (from child)</button>
    </div>
  );
}

Edit how-to-pass-a-method-into-a-child-element-if-the-method-relies-on-state-variable

It's also generally considered anti-pattern to use any sort of "forceUpdate" function and is a code smell. In almost all circumstances if you hit a point where you need to force React to rerender you are doing something incorrect. This is the time you step back and trace through your code to find where a certain piece of state or some prop isn't updated correctly to trigger a rerender naturally.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1