React update upon props changing in depth - javascript

I have a component which takes in a matrix as an input (actually an object which has matrix and getter and setter, but that is irrelevant) and renders a table based on it. The table cells should be text inputs and should change the matrix when the value in them is changed. How do I do that without using a force update? Here is sample working code (this is the App.js in a create-react-app):
import React, {useState} from "react";
const nodes = [
[1,2,3],
[4,5,6],
[7,8,9]
];
function useForceUpdate() {
const [flag, setFlag] = useState(false);
return () => setFlag(!flag);
}
function Component({nodes}) {
const forceUpdate = useForceUpdate();
const handleOnClick = (i,j) => {
nodes[i][j]++;
forceUpdate();
};
return (
<thead>
{nodes.map((row,i)=>
<tr>
{row.map((el,j)=>
<td onClick={()=>handleOnClick(i, j)}>{el}</td>
)}
</tr>
)}
</thead>
);
}
function App() {
return (
<Component nodes={nodes} />
);
}
export default App;
You can try it on CodePen from this link: https://codepen.io/askenderski/pen/dyNRyxJ

I think the right way to go about it would be to have nodes as a state and pass it and a setNodes as a prop to your Component and then trigger the update from your child like so (take a note of the newNode as a copy of nodes) :-
function Component({nodes,setNodes}) {
const handleOnClick = (i,j) => {
let newNodes = nodes.map(row=>[...row]);
newNodes[i][j]+=1;
setNodes(newNodes);
};
return (
<thead>
{nodes.map((row,i)=>
<tr>
{row.map((el,j)=>
<td onClick={()=>handleOnClick(i, j)}>{el}</td>
)}
</tr>
)}
</thead>
);
}
function App() {
const [nodes,setNodes] = React.useState([
[1,2,3],
[4,5,6],
[7,8,9]
]);
return ( <Component nodes={nodes} setNodes={setNodes} />
);
}
ReactDOM.render(
<App></App>,
document.getElementById('root')
);

So if your source of truth comes from above (props instead of state) then you need to pass down a handler as well:
const SomeParentComponent = () => {
// making an assumption that the immediate parent
// holds the nodes as state - it could come from anywhere
// "above" your component
const [matrix,setMatrix] = useState(theMatrix);
// this is the handler that will update your state
const handleChangeNode = (i,j) => {
// always reurn a new matrix, which is why you do matrix.map
setMatrix(matrix => matrix.map((row,idx) => {
// not interested in changing this one, return the same reference
if(idx !== i) return e;
// change this one
return row.map((col,idx2) => {
// not interested in changing this one
if(idx2 !== j) return col;
// change this one
return col++;
});
});
}
return <YourComponent nodes={matrix} handleChangeNode={handleChangeNode}/>
}
Don't mutate stuff in react. If you want to update an object/array (or some property nested within that object/array) always return a new object/array.

Just useState is enough for that
Demo link
const [nodeList,setNodeList] = React.useState(nodes);
const handleOnClick = (i,j) => {
let nodesCopy = [...nodeList];
nodesCopy[i][j]++;
setNodeList(nodesCopy)
};

Related

React array hooks don't work, new values don't display

import React, { useState } from 'react';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import { sections } from '../../data/sections';
export const NavigationTop = () => {
const [mySections, setMySections] = useState(sections);
const selectSection = (id) => {
let newSections = mySections;
newSections[id].name = mySections[id].name + '*';
setMySections(newSections);
};
return (
<Tabs defaultActiveKey="0" id="fill-tab-example" className="mb-3" onSelect={(k) => selectSection(k)} fill>
{mySections.map((el) => {
const { id, name } = el;
return (
<Tab id={id} key={id} eventKey={id} title={name}></Tab>
);
})}
</Tabs>
);
}
The selectSection event is triggered and newSections contains the new values, but the page does not show the new values.
Where is my error?
You are mutating the state object and not providing a new array reference for React's reconciliation process to trigger a component rerender.
const [mySections, setMySections] = useState(sections);
const selectSection = (id) => {
let newSections = mySections; // <-- reference to state
newSections[id].name = mySections[id].name + '*'; // <-- mutations
setMySections(newSections); // <-- same reference
};
The mySections state reference never changes so React bails on rerendering the component. Shallow copy all state, and nested state, that is being updated.
Use a functional state update to correctly update from any previous state.
Example:
const selectSection = (id) => {
setMySections(sections => sections.map(section =>
section.id === id
? { ...section, name: section.name + "*" }
: section
));
};
try this change
let newSections = [...mySections];
what this does is make a copy of your array.
if you don't make a copy, reference doesn't change, and react does a shallow comparison ( only checks the reference and not value ) to see if UI needs to be updated.

How to prevent setting a state variable to another state variable?

I have a controlled component that I call Note. I want its default value to be equal to the selected note (which is set in App.js and passed through as a prop). It seems redundant/bad practice. Here's my code, simplified to the relevant parts. How can I set the default value of textarea to be equal to another state variable?
Edit: Forgot to mention that selectedNote is changed in another component. It works for the state set in useEffect but not for the updates.
App.js
function App(){
const [selectedNote, setSelectedNote] = useState("")
useEffect(() => {
async function fetchData(){
let req = await fetch("http://localhost:9292/notes");
let res = await req.json();
setSelectedNote(res[0])
}
fetchData()
},[])
return (
<Note selectedNote={selectedNote.body}/>
)
}
Note.js
function Note({selectedNote}) {
const [editValue, setEditValue] = useState(selectedNote)
return (
<form>
<textarea value={editValue} onChange={handleChange}>
</textarea>
</form>
)
}
(To clarify, I have no issues if I write const [editValue, setEditValue] = useState("testing123") or some other string)
So ideally you want to lift state up so that the parent component manages the state updates, and the Notes component is as dumb as possible.
In this example the data is loaded into state, and then the notes are built, only receiving an id, some body text which will be their value, and an onChange handler.
When the text is changed, the state is copied, the object in the array (defined by the id) updated, and the new array pushed back into state.
const { useEffect, useState } = React;
const json = '[{"id":1,"body":"Note1"},{"id":2,"body":"Note2"},{"id":3,"body":"Note3"}]';
function mockApi() {
return new Promise(res => {
setTimeout(() => res(json), 2000);
});
}
function Example() {
const [ notes, setNotes ] = useState([]);
const [ selectedNote, setSelectedNote ] = useState(null);
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => setNotes(data));
}, []);
function handleChange(e) {
const { value, dataset: { id } } = e.target;
const copy = [...notes];
copy[id - 1].body = value;
setNotes(copy);
}
function handleClick() {
console.log(JSON.stringify(notes));
}
if (!notes.length) return 'Loading';
return (
<div>
{notes.map(note => {
return (
<Note
key={note.id}
id={note.id}
body={note.body}
handleChange={handleChange}
/>
)
})}
<button onClick={handleClick}>
View state
</button>
</div>
);
}
function Note({ id, body, handleChange }) {
return (
<textarea
data-id={id}
value={body}
onChange={handleChange}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can provide a function to useState that will only be invoked once, when the component renders. Use that function to copy the prop value into the Note's private state.
const [editValue, setEditValue] = useState(() => selectedNote)
You may have other problems, such as the prop value being blank on initial render, but this is still usually the most straightforward way to initialize a private state var based on a prop.
If it turns out that blank-initial-state is an insurmountable problem, then you may instead need to set up a useEffect that updates the private state when the prop value changes to a satisfactory value. Something like this:
const [editValue, setEditValue] = useState()
React.useEffect(() => {
// only update state if old is blank & new is not
if(!editValue && selectedNote) setEditValue(selectedNote)
}, [selectedNote])

Wait for change of prop from parent component after changing it from a child in React

I have rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>

Update the sibling component only

In the code below, how can I update only Component B and not Component A (or the Parent Component)
Component A:
const A = ({ data, clickCallback }) => {
console.debug('A');
return (<button onClick={clickCallback}>Component A</button>)
};
Component B:
const B = ({ filteredData }) => {
console.debug('B');
return <h1>Component B: {filteredData}</h1>;
};
Parent Component:
function Parent() {
console.debug('parent');
const [data, setData] = useState(0);
const handleClick = () => {
setData(data + 1);
};
return (
<div>
<A clickCallback={handleClick}/>
<B filteredData={data} />
</div>
);
}
So when clicking on the Component A button, I only see console.debug('B') in the console?
parent
A
B
B
B
...
Here is the link to the working code: https://codesandbox.io/s/musing-lehmann-kme8d?file=/src/index.js:233-245
NOTE: I have tried wrapping the handleClick inside a useCallback() but, still in the console I see:
parent
A
B
parent
A
B
...
With functional components, they are rerendered when their parent component rerenders. I.E. When Parent rerenders, then both A and B` will be rerendered in the "render phase" in order to compute a diff. This should not be confused with rendering to the DOM during the "commit phase".
You can wrap A in the memo Higher Order Component and pass a custom equality function that returns true/false if the previous and next props are equal.
memo
By default it will only shallowly compare complex objects in the props
object. If you want control over the comparison, you can also provide
a custom comparison function as the second argument.
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
I suggest also correctly console logging in an useEffect so you truly know when the component is rerendered to the DOM.
A functional state update should also be used to update the data state so the state value isn't closed over in the callback scope.
const A = ({ data, clickCallback }) => {
useEffect(() => {
console.debug("A");
});
return <button onClick={clickCallback}>Component A</button>;
};
const MemoA = memo(
A,
(prevProps, nextProps) => prevProps.data === nextProps.data
);
const B = ({ filteredData }) => {
useEffect(() => {
console.debug("B");
});
return <h1>Component B: {filteredData}</h1>;
};
function Parent() {
useEffect(() => {
console.debug("parent");
});
const [data, setData] = useState(0);
const handleClick = () => {
setData(data => data + 1); // <-- functional update
};
return (
<div>
<MemoA clickCallback={handleClick} />
<B filteredData={data} />
</div>
);
}

React: function comes back as undefined

Summary
I have the following function inside of a functional component which keeps coming back undefined. All of the data inside the function, tableData and subtractedStats are defined and accurate.
This is probably just a small JavaScript I'm making so your help would be greatly appreciated!
Code
This is a functional component below:
const TableComponent = ({ tableData }) => {
formatTableData = () => {
console.log("inside sumDataFormat", tableData);
return tableData.forEach(competitor => {
let subtractedStats = [];
console.log("competitor in", competitor);
for (const i in competitor.startingLifeTimeStats) {
if (competitor.startingLifeTimeStats[i]) {
competitor.stats
? (subtractedStats[i] =
competitor.stats[i] - competitor.startingLifeTimeStats[i])
: (subtractedStats[i] = 0);
}
}
console.log("subtractedStats", subtractedStats);
return subtractedStats;
});
};
useEffect(() => {
console.log("formatTableData", formatTableData());
});
}
Edit:
Can someone help me to what's wrong in this code (how to solve this?) and could briefly explain a functional component
The forEach function doesn't not return anything, it simply iterates over your array, giving you an undefined, the map function could be what you were looking for :
formatTableData = () => {
console.log("inside sumDataFormat", tableData);
return tableData.map(competitor => { // This returns another array where every element is converted by what's returned in the predicate
Functional Component are the most basic kind of React component, defined by the component's (unchanging) props.
Functional Component needs return some JSX code (UI) (can be null too).
Here's an example of most basic Functional Component
const App = () => {
const greeting = 'Hello Function Component!';
return <Headline value={greeting} />;
};
const Headline = ({ value }) => {
return <h1>{value}</h1>;
};
export default App;
Solution Code
Here's the solution of the above example as a Functional Component
This is solution below uses hooks to save data to component's state and also uses lifecycle methods to parse that data on componentDidMount:
const TableComponent = (props: ) => {
const [state, setState] = useState(initialState)
// componentDidUpdate
useEffect(() => {
setState(getData(props.data));
}, []);
// getData() - Parser for data coming inside the functional Component
const getData = (tableData) => {
return tableData.map(competitor => {
return competitor.startingLifeTimeStats.map((item, index) => {
return item && competitor.stats ? competitor.stats[index]-item : 0;
})
})
};
// UI (JSX)
return (
<Text>{JSON.stringify(state)}</Text>
);
}
export default TableComponent;
Try with map sample code as below.
render() {
return (<div>
{this.state.people.map((person, index) => (
<p>Hello, {person.name} from {person.country}!</p>
))}
</div>);
}

Categories