Updating state causing tests to not stop - javascript

I have a React application. I am using Jest and React Testing library for unit testing.
I have to test a component. In the useEffect of the component, there is an API call made and once the response is received, we update the component's local state.
const [data, setData] = useState({})
useEffect(()=>{
// Make API call to a custom fetch hook
},[])
useEffect(()=>{
setData(response.data) //response data is a JSON object
},[response])
The test files code snippet is as below -
const {getByTestId} = render(<MyComponent></MyComponent>)
I have not put any assertions yet because of the inifinite running test cases
What have I done? I have been able to mock the fetch call and execute setData.
The problem - The tests keep running forever. But if I change the response.data to some boolean or string or number, the tests do not run infinitly.
Also, if I put a dummy object in the initialization of the state, the tests run fine.
const [data, setData] = useState({
name: 'Test',
Age: '99'
})

Providing an object as dependency in useEffect is not a good idea, since even if the data in object remains same, on every render -- object reference changes - the effect will run again (even if the data within stays same).
A workaround for this would be stringifying the dependency with JSON.stringify. (although doing on data containing some objects like dates, symbols, null or undefined etc. isn't recommended)
useEffect(() => {
setData(response.data)
}, [JSON.stringify(response)]);
Doing above shouldn't affect your UI.
Other solution would be to store the previous value of response and compare before you do setData. You can use usePrevious hook:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

Related

How can I have react batch multiple setStates for fetched data from useEffect?

I am still trying to wrap my head around how react handles renders and this particular behavior has had me scratching my head all day. When i run this code, I get 3 console logs. The first is a null, as expected since useEffect didn't run yet. Next, I get the fetched worldData array from my api call as expected. However, I then get a third console log with the same said array, which leads me to believe my component is being rerendered. If I add another set state and another api call, I see yet another console log.
function App() {
const [worldData, setWorldData] = useState(null)
const [countriesData, setCountriesData] = useState(null)
useEffect(() => {
const fetchData = async () => {
const [world, countries] = await Promise.all([
fetch("https://disease.sh/v3/covid-19/countries?yesterday=true").then(res => res.json()),
fetch("https://disease.sh/v3/covid-19/all?yesterday=true").then(res => res.json())
])
.catch((err) => {
console.log(err)
})
setWorldData(world)
setCountriesData(countries)
}
fetchData();
}, []);
console.log(worldData);
It seems like react is rendering every time I set a State, which is what I assume it's designed to do. However, I've read elsewhere that react batches multiple set states together when set together in useEffect. So why is my set states not being batched then? Is it because of the asynchronous code? If so, is there a way I can fetch multiple endpoints and set all the retrieved data simultaneously so that my component only needs to rerender once?
A couple bits
(1) If you're ONLY concerned about having a single state update then you can defined a single state and update that as such
const [fetched_data, updateFetchedData] = useState({world:null, countries:null});
...
// where *world* and *countries* are variables containing your fetched data
updateFetchedData({world, countries})
...
fetched_data.world // using the fetched data somewhere
(2) HOWEVER, your code should not care about receiving multiple updates and should cater for it as such...
Your useState should define what an empty result would look like, for example instead of
const [worldData, setWorldData] = useState(null); // probably shouldn't look like this
how about
const [worldData, setWorldData] = useState([]); // this is what an empty result set looks like
And inside whatever component that uses both world and countries data ( I assume one references the other which is why you want both updates at once), just put a conditional statement that if it's not found then put some string 'Loading' or 'NA' or whatever until it's loaded.

How do I make UseEffect hook run step by step and not last?

I have UseEffect hook that fetches data from DB and I want it to run FIRST in my component, but it runs last.
How do I make it run before "console.log(titleee)"?
Code:
const [cPost, setCPost] = useState([]);
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
console.log("test");
})
}, []);
const titleee = cPost.title;
console.log(titleee);
I don't think that's the correct path that you want to take.
In order to show the cPost on your page after the request /posts/+postId finished you can opt-out for two following options.
You can show a "loader" to the user if the cPost data is crucial for your whole component.
const [fetchingCPost, setFetchingCPost] = useState(false)
const [cPost, setCPost] = useState({});
const postId = id.match.params.id;
useEffect(() => {
setFetchingCPost(true)
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setFetchingCPost(false)
setCPost(posts.data);
})
}, []);
return fetchingCPost && <div>Loading</div>
Or you can have some default values set from the start for cPost. Just to make sure that your code doesn't break. I think the first solution might be more UX acceptable.
const [cPost, setCPost] = useState({title: '', description: ''});
If you want to store title as a separate variable you can use useMemo for instance or do it via useState same as with cPost. But even then you can't "create" it after the request finishes, you can simply change its value.
In case you want to use useMemo you can make it dependent on your cPost.
const cPostTitle = useMemo(() => {
return !!cPost.title ? cPost.title : ''
}, [cPost])
You have to change you'r way of thinking when programming in react. It is important to know how react works. React does not support imperative programming, it rather support declarative and top down approach , in which case you have to declare your markup and feed it with you'r data then the only way markup changes is by means of changing you'r data. So in you'r case you are declaring a watched variable using useState hook const [cPost, setCPost] = useState([]); , this variable (cPost) has initial values of [] then react continues rendering you'r markup using initial value, to update the rendered title to something you get from a network request (eg: a rest API call) you use another hook which is called after you'r component is rendered (useEffect). Here you have chance to fetch data and update you'r state. To do so you did as following :
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
})
}, []);
this code results in a second render because part of data is changes. Here react engine goes ahead and repaint you'r markup according data change.
If you check this sandbox you'll see two console logs , in first render title is undefined in second render it's something we got from network.
Try adding async/await and see if it works. Here is the link btw for your reference https://medium.com/javascript-in-plain-english/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435

How to prevent race conditions with react useState hook

I have a component that uses hooks state (useState) api to track the data.
The object looks like this
const [data,setData] = React.useState({})
Now I have multiple buttons which make API requests and set the data with the new key
setAPIData = (key,APIdata) => {
const dup = {
...data,
[key]:APIdata
}
setData(dup)
}
Now if I make multiple requests at the same time , it results in race conditions since setting state in react is asynchronous and I get the previous value.
In class-based components, we can pass an updater function to get the updated value, how to do this hooks based component.
You must use setData with a function as its argument. Then it will always get the previous state, no matter what order it will be called.
const [data,setData] = React.useState({})
setData(prevData => ({
...prevData,
[key]: APIdata
}));
Documentation: somewhat hidden in hook api reference.
reactjs.org/docs/hooks-reference.html#functional-updates

How can I fetch data from a Websocket in React?

I am trying to get realtime data from bitstamp API and store in my orders state. The page gets stuck in a loop and drains resources and react doesn't re-render the page when the orders state change. I can see the data if I log it to the console
This is what I have implemented so far.
const [loading, setLoading] = useState(true);
const [orders, setOrders] = useState([]);
const [subscription, setSubscription] = useState({
event: 'bts:subscribe',
data: {
channel: 'order_book_btcusd'
}
});
const ws = new WebSocket('wss://ws.bitstamp.net');
const initWebsocket = () => {
ws.onopen = () => {
ws.send(JSON.stringify(subscription));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
switch (response.event) {
case 'data':
setOrders(response.data);
setLoading(false);
break;
case 'bts:request_reconnect':
initWebsocket();
break;
default:
break;
}
};
ws.onclose = () => {
initWebsocket();
};
};
useEffect(() => {
initWebsocket();
}, [orders, subscription]);
console.log(orders);
const showResult = () => {
orders.bids.map((el, index) => (
<tr key={index}>
<td> {el[0]} </td>
<td> {el[1]} </td>
</tr>
));
};
This is happening because useEffect execute its callback after each render cycle i.e it runs both after the first render and after every update. So for every first message received it is opening a new WebSocket connection and storing the data in the state which is causing a loop.
You can read more about useEffect here
Edited:-
useEffect(() => {
initWebsocket();
}, [orders, subscription]);
The optional second argument to useEffect is used to detect if anything has changed or not (basically it compares prev state/props and given state/props) and it calls the effect whenever there is a change in value.
So on every orders state update, this effect will get called and which in turn causes a loop.
Solution:-
But in your case, you want to establish WebSocket connection only once after the component has mounted and keep listening to the incoming data irrespective of any state or prop change.
You can pass an empty [] such that it gets called only once on mount and unmount.
useEffect(() => {
initWebsocket();
// cleanup method which will be called before next execution. in your case unmount.
return () => {
ws.close
}
}, []);
From doc:-
This requirement is common enough that it is built into the useEffect Hook API. You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect.
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.
If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often.
In useEffect, check if the WebSocket connection is closed before initializing it.
If you are confused with the working of react hooks, you can use class components and initialize your WebSocket connection in componentDidMount and componentDidUpdate(Check if the connection is closed and initialize it).
PS:
I have implemented a simple Chat Application using React and WebSockets.
https://github.com/Nikhil-Kumaran/ChatApp
Go through the repo to have a better idea.
Related component: https://github.com/Nikhil-Kumaran/ChatApp/blob/master/src/WebSockets.js

Unable to set state hook using useDispatch hook in React

I am using useEffect hook to dispatch action to my redux store on component load. Here is code:
const dispatch = useDispatch()
const hotelList = useSelector(state => state.hotels)
const [updatedList, setUpdatedList] = useState([...hotelList])
useEffect(() => {
dispatch(fetchHotels())
// setUpdatedList(hotelList) -- I've tried using this line here but still gets empty array
}, [])
It works fine because when I try to do {console.log(hotelList} I get array of objects from the redux store.
However, when I try to do setUpdatedList(hotelList) or even set initial state it does not work and I get empty array. How can I fix it?
P.S just to clarify I am 100% sure that action was correctly dispatched to the store as I can see the results in console.log as well as in redux dev tools.

Categories