skip to Main Content

This question is more for understanding more how react handles and reacts to changes, than implementation, therefore I’m letting immutable-props-apprach go for a little bit.

I’m trying to get the first element of an array and remove it from the original array, which was passed to component as a prop:

const ChildComponent = (
  arrayToChange: Array<any>[]
) => {
  const elementFromArray = arrayToChange.shift();
}

From the definition of the shift() method::

The shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.

Even though the elementFromArray variable now contains the element from array, the array is intact, it was not affected in any way and still contains all the elements.

But how is that possible? React should pass the props by reference, therefore the original array should be affected. I’d understand if React had some protective measures in place and the changes wouldn’t be reflected in the parent, however, I’d still expect changes being reflected in the child.
I cannot find anything useful which would explaing this behaviour, majority of the resources only mention the immutable approach to props and how to find a way around it, not the reasons or logic behind it.

Even though the elementFromArray variable now contains the element from array, the array is intact, it was not affected in any way and still contains all the elements. However, if I use push() method, then the changes are reflected, arrayToChange contains one more element.

My question then is – why does the arrayToChange react differently to these methods? If shift() doesn’t change the contents, I’d expect push() wouldn’t either.

2

Answers


  1. I am not entirely sure what this question is but I am going on a limb here to take a guess at it so please comment here and I will change before downvoting.

    I think you should try this, in your child component:

    const [data, setData] = React.useState(arrayToChange);
    
    React.useEffect(() => setData(arrayToChange), [arrayToChange.length]);
    

    Then use "data" to map through to output in your jsx

    Then in your parent component, do the shift on the arrayToChange. You can think of the useEffect as a "watcher" which will fire when the length of the array changes.

    Login or Signup to reply.
  2. function Parent() {
      const forceUpdate = useForceUpdate();
      const [letters] = React.useState(["a", "b", "c"]);
    
      return (
        <div className="bg-yellow">
          A: {JSON.stringify(letters)}
          <ChildShowArray array={letters} />
          B: {JSON.stringify(letters)}
          <ChildChangeArray arrayToChange={letters} />
          C: {JSON.stringify(letters)}
          <ChildShowArray array={letters} />
          D: {JSON.stringify(letters)}
    
          <hr />
          <button type="button" onClick={forceUpdate}>re-render</button>
        </div>
      );
    }
    
    function ChildChangeArray({ arrayToChange }) {
      const elementFromArray = arrayToChange.shift();
      
      return (
        <div className="bg-red">
          elementFromArray = {JSON.stringify(elementFromArray)}
        </div>
      );
    }
    
    function ChildShowArray({ array }) {
      return (
        <div className="bg-green">
          array = {JSON.stringify(array)}
        </div>
      );
    }
    
    // helper hook
    function useForceUpdate() {
      const [_state, setState] = React.useState({});
      return React.useCallback(() => { setState({}) }, []);
    }
    
    ReactDOM.createRoot(document.querySelector("#root")).render(<Parent />)
    .bg-yellow { background: yellow }
    .bg-red { background: red }
    .bg-green { background: green }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <div id="root"></div>

    The behaviour in the snippet can be explained if you see the rendering process as a breadth-first algorithm.

    JSX will convert:

    <div className="bg-yellow">
      A: {JSON.stringify(letters)}
      <ChildShowArray array={letters} />
      B: {JSON.stringify(letters)}
      <ChildChangeArray arrayToChange={letters} />
      C: {JSON.stringify(letters)}
      <ChildShowArray array={letters} />
      D: {JSON.stringify(letters)}
    </div>
    

    Into the following JavaScript:

    React.createElement("div", { className: "bg-yellow" },
      "A: ", JSON.stringify(letters),
      React.createElement(ChildShowArray, { array: letters }),
      "B: ", JSON.stringify(letters),
      React.createElement(ChildChangeArray, { arrayToChange: letters }),
      "C: ", JSON.stringify(letters),
      React.createElement(ChildShowArray, { array: letters }),
      "D: ", JSON.stringify(letters),
    )
    

    React.createElement(ChildShowArray, { array: letters }) creates a structure that does not immediately invoke the ChildShowArray component. It will create some sort of intermediate structure/object that only runs whenever the renderer asks it to run.

    JavaScript placed within {...} (JSX context) are passed directly as arguments, and therefore are resolved directly. Meaning that all {JSON.stringify(letters)} inside Parent runs before any of the code in child components runs.

    When building the parent structure is complete, the renderer will visit each intermediate structure/object and asks it to render. This is done from top to bottom, which is why the first ChildShowArray render does still show the full array. Then ChildChangeArray is rendered which removes the first element. The second ChildShowArray render reflects this change, and is rendered without the first element.

    Note that shift() does change the contents of letters, but when it’s called the content of Parent is already rendered and no longer changes. This change does impact Parent on the next render (click "re-render" button in snippet). It also impacts renders of other child components below it that use the same array reference.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search