Keep state in sync with fast occuring events - javascript

I made a custom hooks keep track of a state variable that is based on the amount of socket events received.
When I test by sending 10 simultaneous events the result of the state variable total is 6 or 7 or 8 not the expected 10.
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
useEffect(() => {
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
return (): void => {
Socket.off(event);
};
});
return total;
}
The logs of run look something like
0
1
1
2
3
3
4
4
4
5
Socket in the example above is implementation around websocket.
So I can deduct that total is not updated fast enough, but what is the best pattern to handle this sort of behaviour ?

Socket.on event has to be outside the useEffect function
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
useEffect(() => {
return (): void => {
Socket.off(event);
};
}, []);
return total;
}

Try putting an empty array as the second argument in the hook. You don't want this to register an event each time the component renders.
const useSocketAmountReceivedState = (event: string): number => {
const [total, setTotal] = useState(0);
useEffect(() => {
Socket.on(event, () => {
console.log(total);
setTotal(total + 1);
});
return (): void => {
Socket.off(event);
};
}, [total]);
return total;
}
UPDATE:
I made an update to my initial answer, and added Total into the dependency array of the React Hook.
Note that the second argument, aka dependency array. It is an array that accepts state or prop. And it instructs React to run this hook each time any of the elements in the dependency array changes.
If you pass an empty array, then the hook will only be run at initial load, after the component mounts.
In your example, if you pass an empty array, it creates a closure. Hence, the total value will always be the initial value.
So you can pass Total into the dependency array, which will invoke the useEffect() to run only when Total value changes. In your example, where there is no dependency array passed to the second argument, the useEffect() will run every single time, which is not what we want.

A colleague came with this solution.
Using a reference for which as it is a reference is not enclosed.
const useSocketAmountReceivedState = (event: string): number => {
const count = useRef(0);
const [state, setState] = useState(0);
useEffect(() => {
Socket.on(event, () => {
count.current++;
setState(count.current);
});
return (): void => {
Socket.off(event);
};
}, []);
return state;
}

Related

Is there any hooks method in ReactJS like this.setState() in class components?

I want to set multiple states and call the useEffect function at once. Here's some code for better understanding.
const [activePage, setActivePage] = useState(1);
const [skip, setSkip] = useState(0);
const [limit, setLimit] = useState(10);
const getUserList = ()=>{
// do something
}
useEffect(() => {
getUserList();
}, [activePage, skip, limit]);
Here you can see I have three dependencies. setting all dependencies calls the getUserList() three times. all three dependencies are independent.
I want to call the getUserList() only once when I need to change all three states, similar to this.setState() in class components like:
this.setState({
activePage: //new value,
skip: // new value,
limit: // new value
},()=>getUserList())
Can someone please let me know if there is any method to achieve this?
This might not be the best solution but I think it should work. I imagine that when the state is changing three times at once, it's a special case, so you could create another state that would store a boolean that would be set to true in the case that it does happen. Then, the useEffect could be set for that boolean instead of all three states.
Special case where all 3 states are being updated:
setActivePage()
setSkip()
setLimit()
setAllChanged(true)
...
useEffect(() => {
getUserList();
}, [allChanged])
For your example, maybe immer.js will solve it better as your intended.
import produce from 'immer'
function FooComponent() {
const [state, setState] = useState({
activePage: 1,
skip: 0,
limit: 10
})
const getUserList = () => {}
// use immer to assign `payload` to `state`
const changeState = (payload) => {
setState(produce(draft => {
Objest.keys(payload).forEach(key => {
draft.key = payload.key
})
}))
}
changeState({
activePage: 2
})
useEffect(() => {
getUserList();
}, [state.activePage, state.skip, state.limit]);
return (< />)
}

Async/Await in useEffect(): how to get useState() value?

I have the following code snippet. Why is my limit always 0 in my fetchData? If I were to console.log(limit) outside of this function it has the correct number. Also If I dont use useState but a variable instead let limit = 0; then it works as expected
I also added limit as a dependency in useEffect but it just keeps triggering the function
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const fetchData = async () => {
console.log(limit);
const { data } = await axios.post(endpoint, {
limit: limit,
});
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
};
useEffect(() => {
fetchData();
window.addEventListener(`scroll`, (e) => {
if (bottomOfPage) {
fetchData();
}
});
}, []);
When you pass an empty dependency array [] to useEffect, the effect runs only once on the initial render:
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
useEffect docs
The initial state of limit is 0 as defined in your useState call. Adding limit as a dependency will cause the effect to run every time limit changes.
One way to get around your issue is to wrap the fetchData method in a useCallback while passing the limit variable to the dependency array.
You can then pass the function to the dependency array of useEffect and also return a function from inside of useEffect that removes event listeners with outdated references.
You should also add a loading variable so that the fetchData function doesn't get called multiple times while the user is scrolling to the bottom:
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
console.log(limit);
// Prevent multiple endpoint calls when scrolling near the end with a loading state
if (loading) {
return;
}
setLoading(true);
const { data } = await axios.post(endpoint, { limit });
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
setLoading(false);
}, [limit, loading]);
// Make the initial request on the first render only
useEffect(() => {
fetchData();
}, []);
// Whenever the fetchData function changes update the event listener
useEffect(() => {
const onScroll = (e) => {
if (bottomOfPage) {
fetchData();
}
};
window.addEventListener(`scroll`, onScroll);
// On unmount ask it to remove the listener
return () => window.removeEventListener("scroll", onScroll);
}, [fetchData]);

Back to back useState not updating first state, only the second one

I am trying to have a general function which run setState twice, however, the first setState will not update. Are there any way to get around it? or how to fix this issue?
Parent
const [data, setData] = useState({});
const updateData = (key, value) => {
console.log(key, value);
setData({ ...data, [key]: value });
};
...
<div>
Num 1: {data.num1}, Num2: {data.num2}
</div>
<Child updateData={updateData} />
Child
const { updateData } = props;
const onClick = () => {
updateData("num1", 1);
updateData("num2", 2);
};
return <button onClick={onClick}> Click here </button>
console.log return both values being called, but only 1 value being updated
codesandbox example here
(After some testing, even calliing both in the same parent function, if calling setData twice, it still wont work (see Simplify.js)
While you could use a callback so that the argument contains the currently updated state, including prior state setters that've run but before a re-rendering has occurred:
const updateData = (key, value) => {
setData(data => ({ ...data, [key]: value }));
};
If you have a limited number of possible properties in the data variable, consider using separate states instead:
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const onClick = () => {
setNum1(num1 + 1);
setNum2(num2 + 2);
};
setState is asynchronous so if you do the two calls one after the other in the same function the state won't have updated for the second call and you won't get what you want.
The best practise in general for when setting state based off previous state is to use a callback.
const updateData = (key, value) => {
setData(prevData => { ...prevData, [key]: value });
};
const onClick = () => {
updateData("num1", 1);
updateData("num2", 2);
};

How to handle an unchanging array in useEffect dependency array?

I have a component like this. What I want is for the useEffect function to run anytime myBoolean changes.
I could accomplish this by setting the dependency array to [myBoolean]. But then I get a warning that I'm violating the exhaustive-deps rule, because I reference myArray inside the function. I don't want to violate that rule, so I set the dependency array to [myBoolean, myArray].
But then I get an infinite loop. What's happening is the useEffect is triggered every time myArray changes, which is every time, because it turns out myArray comes from redux and is regenerated on every re-render. And even if the elements of the array are the same as they were before, React compares the array to its previous version using ===, and it's not the same object, so it's not equal.
So what's the right way to do this? How can I run my code only when myBoolean changes, without violating the exhaustive-deps rule?
I have seen this, but I'm still not sure what the solution in this situation is.
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
useEffect(() => {
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]
}
Solution 1
If you always need the 1st item, extract it from the array, and use it as the dependency:
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const item = myArray[0];
useEffect(() => {
if(myBoolean) {
setMyString(item);
}
}, [myBoolean, item]);
}
Solution 2
If you don't want to react to myArray changes, set it as a ref with useRef():
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const arr = useRef(myArray);
useEffect(() => { arr.current = myArray; }, [myArray]);
useEffect(() => {
if(myBoolean) {
setMyString(arr.current);
}
}, [myBoolean]);
}
Note: redux shouldn't generate a new array, every time the state is updated, unless the array or it's items actually change. If a selector generates the array, read about memoized selectors (reselect is a good library for that).
I have an idea about to save previous props. And then we will implement function compare previous props later. Compared value will be used to decide to handle function change in useEffect with no dependency.
It will take up more computation and memory. Just an idea.
Here is my example:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function arrayEquals(a, b) {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index]);
}
const MyComponent = ({ myBoolean, myArray }) => {
const [myString, setMyString] = useState('');
const previousArray = usePrevious(myArray);
const previousBoolean = usePrevious(myBoolean);
const handleEffect = () => {
console.log('child-useEffect-call-with custom compare');
if(myBoolean) {
setMyString(myArray[0]);
}
};
//handle effect in custom solve
useEffect(() => {
//check change here
const isEqual = arrayEquals(myArray, previousArray) && previousBoolean === myBoolean;
if (!isEqual)
handleEffect();
});
useEffect(() => {
console.log('child-useEffect-call with sallow compare');
if(myBoolean) {
setMyString(myArray[0]);
}
}, [myBoolean, myArray]);
return myString;
}
const Any = () => {
const [array, setArray] = useState(['1','2','3']);
console.log('parent-render');
//array is always changed
// useEffect(() => {
// setInterval(() => {
// setArray(['1','2', Math.random()]);
// }, 2000);
// }, []);
//be fine. ref is not changed.
// useEffect(() => {
// setInterval(() => {
// setArray(array);
// }, 2000);
// }, []);
//changed ref but value in array are not changed -> handle this case
useEffect(() => {
setInterval(() => {
setArray(['1','2', '3']);
}, 2000);
}, []);
return <div> <MyComponent myBoolean={true} myArray={array}/> </div>;
}

React useEffect cleanup with current props

I ran into a need of cleaning a useEffect when component is unmounted but with an access to the current props. Like componentWillUnmount can do by getting this.props.whatever
Here is a small example:
Click "set count" button 5 times and look at your console. You'll see 0 from the console.log in component B, regardless "count" will be 5
https://codesandbox.io/embed/vibrant-feather-v9f6o
How to implement the behavior of getting current props in useEffect cleanup function (5 in my case)?
UPDATE:
Passing count to the dependencies of useEffect won't help because:
Cleanup will be invoked on every new count is passed to the props
The last value will be 4 instead of the desired 5
Was referenced to this question from another one so will do a bit of grave digging for it since there is no accepted answer.
The behaviour you want can never happen because B never renders with a count value of 5, component A will not render component B when count is 5 because it'll render unmounted instead of B, the last value B is rendered with will be 4.
If you want B to log the last value it had for count when it unmounts you can do the following:
Note that effects executes after all components have rendered
const useIsMounted = () => {
const isMounted = React.useRef(false);
React.useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
const B = ({ count }) => {
const mounted = useIsMounted();
React.useEffect(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
return () => !mounted.current && console.log(count);
}, [count, mounted]);
return <div>{count}</div>;
};
const A = () => {
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
if (count === 5) {
//B will never be rendered with 5
return <div>Unmounted</div>;
}
return (
<React.Fragment>
<B count={count} />
<button onClick={handleClick}>Set count</button>
</React.Fragment>
);
};
ReactDOM.render(<A />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You need to track count in useEffect:
import React, { useEffect } from "react";
const B = ({ count }) => {
useEffect(() => {
return () => console.log(count);
}, [count]);
return <div>{count}</div>;
};
export default B;
Since you passed empty array as dependency list for useEffect, your effect function only gets registered the first time. At that time, the value of count was 0. And that is the value of count which gets attached to the function inside useEffect. If you want to get hold of the latest count value, you need your useEffect to run on every render. Remove the empty array dependency.
useEffect(() => {
return () => console.log(count);
})
or use count as the dependency. That way, your effect function is created anew every time count changes and a new cleanup function is also created which has access to the latest count value.
useEffect(() => {
return () => console.log(count);
}, [count])

Categories

Resources