This question is more to learn more about how react handles and reacts to changes rather than the implementation, so I let immutable-props-apprach develop a bit.
I'm trying to get the first element of an array and remove it from the original array that is passed to the component as a prop:
const ChildComponent = ( arrayToChange: Array<any>[] ) => { const elementFromArray = arrayToChange.shift(); }
From the definition of shift() method::
shift() method removes the first element from the array and returns the removed element. This method changes the length of the array.
Although the elementFromArray variable now contains the elements in the array, the array is complete, it is not affected in any way and still contains all elements.
But how is this possible? React should pass props by reference, so the original array should be affected. I'd understand if React put some protections in place and those changes wouldn't be reflected in the parent, however, I'd still like the changes to be reflected in the child. I can't find anything useful to explain this behavior, most resources only mention immutable methods for props and how to find a workaround, but not the reason or logic behind it.
Although the elementFromArray variable now contains the elements in the array, the array is complete, it is not affected in any way and still contains all elements. However, if I use the Push() method, the changes are reflected and arrayToChange contains one more element.
My question is - why does arrayToChange react differently to these methods? If shift() doesn't change the content, I hope push() won't either.
P粉1822188602023-09-07 14:26:37
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 behavior in the code snippet can be explained if you think of 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>
Enter 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: letter })
Creates a structure that does not immediately call the ChildShowArray
component. It will create some kind of intermediate structure/object that will only run when the renderer asks it to.
JavaScript placed inside {...}
(JSX context) is passed directly as a parameter and therefore parsed directly. This means that all {JSON.stringify(letters)}
in Parent
are run before any code in the child component is run.
When building the parent structure is complete, the renderer will access each intermediate structure/object and ask it to render. This is done from top to bottom, which is why the first ChildShowArray
render still shows the full array. Then render ChildChangeArray
and remove 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 is called, the contents of Parent
are already rendered and no longer change. This change does affect the Parent
the next time it is rendered (click the "Rerender" button in the snippet). It also affects the rendering of other child components below it that use the same array reference.
P粉2877263082023-09-07 00:57:49
I'm not entirely sure what the issue is, but I'm going to hazard a guess here, so please leave a comment here and I'll change it before voting.
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 to the output in jsx
Then in the parent component, shift arrayToChange. You can think of useEffect as an "observer" that will fire when the array length changes.