I am trying to curb useEffect related to nested components. Here are the components:
Parent (it receives data from API):
const ListOfLots = (props) => {
const initial = {listLots: props.lots, form: props.form}
const [lots, setLots] = useState(initial);
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
});
return (
<div>
{
lots.listLots.map(function(lot) {
return <Lot key={lot.uuid} lot={lot} procForm={lots.form}/>
})
}
</div>
)
}
Nested:
const Lot = (props) => {
const initial = {currLot: props.lot, form: props.form};
const [lot, setLot] = useState(initial);
let getWinningBid = (offers) => {
for (let i = 0; i < offers.length; i++) {
console.log("BOOM!!!");
if (offers[i].isTrue === true) {
return offers[i].pip;
}
}
}
return (
...
)
}
While I am using no dependencies at parent's useEffect, I have got an infinite invoking of console.log("BOOM!!!"), that is, of course, unacceptable, but my Nested component rerendered. When I try to use the following type of dependencies at useEffect: [], [lots.form], [lots.listLots] or [lots.listLots.length] my Nested component is not rerendered: it stays blank. So the result is the following: I have an infinite useEffect loop or not-working(?) useEffect.
Is there any way in this case to handle the useEffect?
Use
useEffect(() => {
setLots({
listLots: props.lots,
form: props.form
});
}, [props.lots, props.form]);
This triggers the callback only if the value of props.lots, props.form is changed else it won't be triggered on every rerender as in case of no second argument.
A similar question here might help you find better explanations.
Related
I was wondering if it's possible to update state multiple times in a single event. Say I have a form where multiple inputs may need to be used to update a single state based on some logic like the below example:
State defined in parent component:
import React, { useState } from "react";
function ParentComponent() {
const [state, setState] = useState([]);
return (
<ChildComponent state={state} setState={setState} />
)};
export default ParentComponent;
State updated in child component:
import React, { useState } from "react";
function ChildComponent(props) {
const onSubmit = data => {
if(someLogicHere) {
props.setState(_ => [...props.state, data]);
}
if(someOtherLogicHere) {
props.setState(_ => [...props.state, data]);
}
}
return (
//Form goes here
)};
export default ChildComponent;
When using this method, only the output of the first if statement is returned onSubmit and added to the state object. Is there a way to have the output from all possible if statements be added?
I would build the array first then update the state once, something like that:
const onSubmit = data => {
let result = [...props.state];
if(someLogicHere) {
result.push(data);
}
if(someOtherLogicHere) {
result.push(data);
}
props.setState(result);
}
You can use the callback form of setState for that:
function ChildComponent(props) {
const onSubmit = data => {
if(someLogicHere) {
props.setState(state => [...state, data]);
// −−−−−−−−−−−−−−−−−−−−−^^^^^−−−−−−−−^^^^^
}
if(someOtherLogicHere) {
props.setState(state => [...state, data]);
// −−−−−−−−−−−−−−−−−−−−−^^^^^−−−−−−−−^^^^^
}
}
return (
//Form goes here
)};
That ensure that overlapping calls don't result in lost data.
Don't use multiple setState() in all if conditions instead create a combined update object and then merge into state
e.g.
const onSubmit = data => {
const data = {};
if(someLogicHere) {
data['a'] = 1;
}
if(someOtherLogicHere) {
data['b'] = 2;
}
if(anotherLogic) {
data['b'] = data['b'] + 10;
}
props.setState(_ => [...props.state, data]);
}
If someone can help me with this ?
I need this function to store the filteredObject key in the state. but when I call this function in componentDidMount(), it didn't work and when I called it in ComponentDidUpdate() it works but going on a infinite loop?
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => { return agent.email === returnedEmail })
if (filteredEmail) {
this.setState({
agentApplicationId: filteredEmail.properties
})
}
}
}
You need to be very careful when setting state in componentDidUpdate. Calling setState updates the component, which triggers componentDidUpdate, which calls setState, and so on, causing the infinite loop. From the React docs:
You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition...or you’ll cause an infinite loop.
The solution is to add some kind of condition so you're not updating state unnecessarily. For example:
userData = () => {
const returnedEmail = storageManger.getEmailFromStore();
const { agents, agentApplicationId } = this.state;
if (returnedEmail) {
const filteredEmail = agents.find(agent => agent.email === returnedEmail);
// Add an extra condition here to prevent state from updating if the values are already equal.
if (filteredEmail && filteredEmail.properties !== agentApplicationId) {
this.setState({
agentApplicationId: filteredEmail.properties
});
}
}
}
I have a mathematical algorithm that I want to keep separated from React. React would be a view to the state within that algorithm, and should not define the way of how the logic is flowing within the algorithm. Also, since it is separated, it's much easier to unit test the algorithm. I have implemented it using class components (simplified):
class ShortestPathRenderer extends React.Component {
ShortestPath shortestPath;
public constructor(props) {
this.shortestPath = new ShortestPath(props.spAlgorithm);
this.state = { version: this.shortestPath.getVersion() };
}
render() {
... // Render waypoints from shortestPath
}
onComponentDidUpdate(prevProps) {
if (prevProps.spAlgorithm !== this.props.spAlgorithm) {
this.shortestPath.updateAlgorithm(this.props.spAlgorithm);
}
}
onComponentWillUnmount() {
this.shortestPath = undefined;
}
onAddWayPoint(x) {
this.shortestPath.addWayPoint(x);
// Check if we need to rerender
this.setState({ version: this.shortestPath.getVersion() });
}
}
How would I go about this using React hooks? I was thinking about using the useReducer method. However, the shortestPath variable would then be a free variable outside the reducer and the reducer is no longer pure, which I find dirty. So in this case the whole state of the algorithm must be copied with every update on the internal state of the algorithm and a new instance must be returned, which is not efficient (and forces the logic of the algorithm to be the React-way):
class ShortestPath {
...
addWayPoint(x) {
// Do something
return ShortestPath.clone(this);
}
}
const shortestPathReducer = (state, action) => {
switch (action.type) {
case ADD_WAYPOINT:
return action.state.shortestPath.addWayPoint(action.x);
}
}
const shortestPathRenderer = (props) => {
const [shortestPath, dispatch] = useReducer(shortestPathReducer, new ShortestPath(props.spAlgorithm));
return ...
}
You can switch class-based in example in functional analog just using useState hook
function ShortestPathRenderer({ spAlgorithm }) {
const [shortestPath] = useRef(new ShortestPath(spAlgorithm)); // use ref to store ShortestPath instance
const [version, setVersion] = useState(shortestPath.current.getVersion()); // state
const onAddWayPoint = x => {
shortestPath.current.addWayPoint(x);
setVersion(shortestPath.current.getVersion());
}
useEffect(() => {
shortestPath.current.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
// ...
}
I'd go with something like this:
const ShortestPathRenderer = (props) => {
const shortestPath = useMemo(() => new ShortestPath(props.spAlgorithm), []);
const [version, setVersion] = useState(shortestPath.getVersion());
useEffect(() => {
shortestPath.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
const onAddWayPoint = (x) => {
shortestPath.addWayPoint(x);
// Check if we need to rerender
setVersion(shortestPath.getVersion());
}
return (
... // Render waypoints from shortestPath
)
}
you can even decouple logic further and create useShortestPath hook:
reusable stateful logic:
const useShortestPath = (spAlgorithm) => {
const shortestPath = useMemo(() => new ShortestPath(spAlgorithm), []);
const [version, setVersion] = useState(shortestPath.getVersion());
useEffect(() => {
shortestPath.updateAlgorithm(spAlgorithm);
}, [spAlgorithm]);
const onAddWayPoint = (x) => {
shortestPath.addWayPoint(x);
// Check if we need to rerender
setVersion(shortestPath.getVersion());
}
return [onAddWayPoint, version]
}
presentational part:
const ShortestPathRenderer = ({spAlgorithm }) => {
const [onAddWayPoint, version] = useShortestPath(spAlgorithm);
return (
... // Render waypoints from shortestPath
)
}
I have two sibling components that share state via context in react.
The shared state between the components is an array.
If I update arr state in one component, I want the other component to listen for that update and do something accordingly. When I use useEffect in the second component, I listen for changes in the arr state variable.
For example:
// App Component -------
const App = props => {
const { arr, setArr } = useContext(GlobalContext)
const handleChange = () => {
const newArr = arr
[10, 20, 30, 40].map(v => {
newArr.push(v)
setArr(newArr)
})
return (...)
}
// App2 (Sibling) Component
const App2 = props => {
const { arr, setArr } = useContext(GlobalContext)
const [localArr, setLocalArr] = useState(0)
useEffect(
() => {
updateLocalState()
},
// fire if "arr" gets updated
[arr]
)
const updateLocalState = () => {
setLocalArr(localArr + 1)
}
return (...)
}
The useEffect hook is only fired on the initial render, though the state of arr updates.
I know that declaring a new variable const newArr = arr to my state variable is a reference, so newArr.push(v) is technically a state mutation. However, the state still updates, no warning is thrown, and useEffect does nothing.
Why does useEffect not get called though the state gets updated? Is it because of the state mutation?
Second Question: Why is there no warning or error thrown regarding a state mutation? State mutations are dangerous - If it happens, I'd expect some sort of warning.
Live demo here:
The array you pass as second argument to useEffect only checks if the elements in the array are === to the elements in it in the previous render. const newArr = arr; will lead to newArr === arr since it doesn't create a new array, which is not what you want.
Create a new array with all the elements in arr and it will work as expected.
const App = props => {
const { arr, setArr } = useContext(GlobalContext)
const handleChange = () => {
const newArr = [...arr]
[10, 20, 30, 40].forEach(v => {
newArr.push(v)
})
setArr(newArr)
}
return <>{/* ... */}</>
}
When you want to update array using useState hook. Make sure to spread the array into new array and update the new array so that your useEffect listening for this state will be called.
UseEffect will not call in the below code snippet as you are directly updating array.
const [skills, selectedSkills] = useState([])
const onSelect = (selectedList) => {
selectedSkills(selectedList)
}
useEffect(() => {
MyLogger('useEffect called')
}, [skills])
UseEffect will call in the below code snippet as we are keeping new reference to the array.
const [skills, selectedSkills] = useState([])
const onSelect = (selectedList) => {
const tempSelectedList = [...selectedList]
selectedSkills(tempSelectedList)
}
useEffect(() => {
MyLogger('useEffect called')
}, [skills])
So, let talk about lifecycle of a component React.
I have my model file is name is Firebase.js inside I have all my methods can trigger my data.
Firebase.js
export const getUserShare = () => {
let arr = [];
db.ref(`/music/${userID()}`).on("value", (snapshot, key) => {
snapshot.forEach(items => {
let element = items.val();
arr.push(element);
});
});
return arr;
};
And I have a Profil.js component and inside I import the method that I want (For this example _getUserData())
On my component I set a state arrSharing, at the name of my method _getUserData() the output of this is an array with all my data.
Profil.js :
this.state = {
arrSharing: getUserShare()
};
The Profil.js is only accessible with my router or if you put the link on your browser.
Now I rendering the data in my render function like that :
{this.state.arrSharing.map((items, i) => {
return (
<div key={i} className="share-music">
<h5 className="share-music__title">
{items.artiste} - {items.title}
</h5>
<p className="share-music__platform">
par {items.displayName} sur #{items.radio}
</p>
</div>
);
})}
Now running the application and if I go for the first time on the Profil.js component the data was not loaded, BUT if I go on another component and after I return on Profil.js the data was loaded.
The problem come from the component is not rendering for the first time. When I use componentDidMount() I have the same problem. And it's very problematic for my case.
I see there is a problem with asynchronous action handling.
Change your getUserShare method to accept callback:
export const getUserShare = (callback) => {
db.ref(`/music/${userID()}`).on("value", (snapshot, key) => {
const arrSharing = [];
snapshot.forEach(items => {
let element = items.val();
arrSharing.push(element);
});
callback(arrSharing);
});
};
Then do this in Profile.js:
this.state = {
arrSharing: [],
}
componentDidMount() {
getUserShare((arrSharing) => {
this.setState({ arrSharing })
})
}