Unable to access state - React.js working with Websocket - javascript

This is my first post here, so nice to meet you all. I have recently started my adventure with React, so my question probably will not be very challenging (it could also be very basic stuff), however I got stuck with this problem for a while.
So I am working with WebSocket that when connected, sends one-time message with all products currently in stock. Then, every few seconds, it sends object with update of product stock changes.
I managed to update state with first recieved message, but then, when I try to access state in a function handleData, state is empty array. This is happening despite the fact that the data rendered on the page using state is still visible, state is visible in Firefox Developer Edition React DevTools, and the useEffect that is associated with the state change fires only once - at the start, so it doesn't change.
I want to be able to access data I put before in state, in function handleData, so that I can update the state with stock changes. It is interesting that when the websocket loads again, the "products" status variable is extended by the previous, unloaded products.
Here is code of my component:
import React, { useState, useEffect, useRef } from 'react';
import Product from './Product';
export default function ProductsList() {
const [products, setProducts] = useState([]);
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket(<HERE IS WEBSOCKET URL BUT I CENSORED IT>);
ws.current.onmessage = (message) => handleData(message);
return () => ws.current.close();
},[]);
useEffect(() => {
console.log("Products useEffect:", products)
}, [products]) //prints fine here
const handleData = (message) => {
const data = JSON.parse(message.data);
switch (data.operation) {
case 'product.stock.decreased': {
console.log("Products Switch:", products) // prints empty array here
break;
}
default: {
setProducts(prevState => [...prevState, ...data]) // here state updates
break;
}
}
}
return (
<div className="ProductsList">
{products.map(p => <Product key={p.productId} productId={p.productId} name={p.name} price={p.price} stock={p.stock} />)}
</div>
);
}
Many thanks in advance for your help.

Related

localStorage resets to empty on refresh in NextJS

I have a shopping cart system in my next.js app using Context.
I define my cart with useState:
const [cartItems, setCartItems] = useState([]);
Then I use useEffect to check and update the localStorage:
useEffect(() => {
if (JSON.parse(localStorage.getItem("cartItems"))) {
const storedCartItems = JSON.parse(localStorage.getItem("cartItems"));
setCartItems([...cartItems, ...storedCartItems]);
}
}, []);
useEffect(() => {
window.localStorage.setItem("cartItems", JSON.stringify(cartItems));
}, [cartItems]);
This stores the items in localStorage fine, but when I refresh, it resets the cartItems item in localStorage to an empty array. I've seen a few answers where you get the localStorage item before setting the cart state but that throws localStorage is not defined errors in Next. How can I do this?
setCartItems sets the value of cartItems for the next render, so on the initial render it's [] during the second useEffect
You can fix this by storing a ref (which doesn't rerender on state change) for whether it's the first render or not.
import React, { useState, useRef } from "react";
// ...
// in component
const initialRender = useRef(true);
useEffect(() => {
if (JSON.parse(localStorage.getItem("cartItems"))) {
const storedCartItems = JSON.parse(localStorage.getItem("cartItems"));
setCartItems([...cartItems, ...storedCartItems]);
}
}, []);
useEffect(() => {
if (initialRender.current) {
initialRender.current = false;
return;
}
window.localStorage.setItem("cartItems", JSON.stringify(cartItems));
}, [cartItems]);
React never updates state immediately It's an asynchronous process, for example, if you console.log(stateValue) just after the setState() method you'll get prevState value in a log. You can read more about it here.
That is exactly happening here, you have called setState method inside the first useEffect, state has not updated yet by react and we're trying to update localStorage with the latest state value(for now it's [] since react has not updated the state yet). that's why the localStorage value holds an empty array.
For your case, you can skip the first execution of 2nd useEffect as #Samathingamajig's mentioned in his answer.
PS: Thanks #Samathingamajig for pointing out the silly mistake, I don't know how missed that. LOL
thanks to jamesmosier for his answer on gitHub
The issue you are seeing is because localStorage (aka window.localStorage) is not defined on the server side. Next server renders your components, so when that happens and it tried to access localStorage it can't find it. You'll have to wait until the browser renders it in order to use localStorage.
full answer link here
EDIT
you can try this :
useEffect(() => {
if (typeof window !== "undefined") {
const storedCartItems = JSON.parse(localStorage.getItem("cartItems"));
if(storedCartItems !== null) {
setCartItems([...cartItems, ...storedCartItems]);
}
}
}, []);
useEffect(() => {
if (typeof window !== "undefined") {
window.localStorage.setItem("cartItems", JSON.stringify(cartItems));
}
}, [cartItems]);

Infinite renderloop, when changing an array, that was called from an API and put into state

i have a very similiar problem, to a problem, that i had before: How to execute a function AFTER an API-Call was made with setState within useEffect?.
It seems that the useEffect Function, that is calling my API, is running AGAIN after my second useEffect Function was called. So here is what is happening:
quizData is an empty array (which is fine for the beginning)
quizData is an Array filled with data from the API (but without the key, which is fine at this moment)
quizData is then an Array filled with Data from the API and also has the key-property added (so the second useEffect seems to be working). Perfect!
quizData is again the Array only filled with data from the API (the key-property is missing, it seems only the first useEffect is running again) Annoying.
Both solutions of the "old question" are unfortunately not working in this case. The only solution, that was working is to disable the strict-mode of react (then the component is not rendered twice). But its not recommended for beginners to do this, because this workaround seem to be a strong indication, that my code is very unstable.
If i change the dependency-array of the second-useEffect from quizData.length to only quizData. I am getting the infinite renderloop and my complete App is crashing.
If it stays quizData.length the second-useEffect is only running once. Which i dont understand, because i thought EVERY useEffect is running at the beginning independently what is in the dependency-array.
So, is there any solution to this, except to disable Strict-Mode? For me it seems a frequent case, that you call something from an API and want to change the data in it.
Here is my codebase:
import logo from './logo.svg';
import React from "react";
import './App.css';
import Start from "./components/Start.js"
import Questions from "./components/Questions.js"
import {nanoid} from "nanoid"
function App() {
const [start, setStart] = React.useState(true)
const [quizData, setQuizData] = React.useState(() => [])
const isInitialRender = React.useRef(true);
React.useEffect(() => { *//getting the data from the API and put it into an Array*
fetch("https://opentdb.com/api.php?amount=5&type=multiple")
.then(res => res.json())
.then((data) =>
{
isInitialRender.current = false;
setQuizData(data.results);
});
console.log("i am the Boss")
},[])
React.useEffect(() => { *//taking the Array that was created with the API-Call and adding an*
if(!isInitialRender.current) addKey();
}, [quizData.length])
console.log(quizData)
function addKey() {
setQuizData((prevData) => (prevData.map(element => ({...element, key: nanoid()}))))
}
function changeDisplay() {
setStart(prevState => !prevState)
}
return (
<div className="App">
{start ? <Start changeDisplay={changeDisplay}/> : <Questions quizData={quizData} />}
</div>
);
}
export default App;

useEffect causing it to call the method to get posts way too many times. I only want to get the posts when my query changes

I am trying to call the reddit API. The post titles are showing up, but I want them to rerender when my query changes. I just want to know how to call a method when a piece of my state changes(aka my query). I’m using useEffect from react to do it but that calls it whenever anything changes in the component, causing it to call the method to get posts way to many times. I only want to get the posts when my query changes.
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
function Results()
{
const query = useSelector(state => state.query);
const results = useSelector(state => state.results);
const dispatch = useDispatch();
let fetchResults = () =>
{
let postTitles = [];
let postSrcs = [];
fetch('https://www.reddit.com/r/' + query + '.json')
.then(response => response.json())
.then(body => {
for (let i = 0; i < body.data.children.length; ++i) {
if (body.data.children[i].data.post_hint === 'image')
{
let img_url = body.data.children[i].data.url_overridden_by_dest;
postSrcs.push(img_url);
}
let title = body.data.children[i].data.title;
postTitles.push(title);
}
dispatch({type: "QUERY_RESULTS", payload: postTitles})
}).catch((err) => {
console.log(err)
});
}
useEffect(() => {
fetchResults();
console.log("use effect triggered")
})
return (
<>
<h1>Query: {query}</h1>
{ !results
? <h1>No Results</h1>
: results.map(p => <h6> {p} </h6>)
}
</>
)
}
export default Results;
For example in the console log that tells me when use effect is triggered. and when i search for a post the use effect triggered is stacking up.
useEffect has a differents mode. You can check how to use in official document https://reactjs.org/docs/hooks-reference.html#useeffect
So the main you must know is 3 things
useEffect is the last render in react. So first render a components and read other code when it finish useEffect run.
useEffect may run code only one time adding []. for example
useEffect ( () => {
...code
}, [])
This code will run only one time.
useEffect may run code watching variables adding variables into []. For example
useEffect ( () => {
...code
}, [ count, name , ... ])
This code will run first time and later would run if count or name change
To achieve that you need to prevent useEffect to be called on any changes, and only once the query changes.
NOTE: Since you're using dispatch within fetchResults, it's better to make sure that dispatch is ready before calling fetchResults.
Your useEffect may look like the following to achieve that:
useEffect(() => {
// To prevent call fetchResults if dispatch only is changed
if (query) {
fetchResults();
console.log("use effect triggered");
}
}, [dispatch, query]);
Hooks like useEffect are used in function components. The Class component comparison to useEffect are the methods componentDidMount, componentDidUpdate, and componentWillUnmount.
useEffect will run when the component renders, which might be more times than you think.
So useEffect takes a second parameter
The second param is an array of variables that the component will check to make sure changed before re-rendering. You could put whatever bits of props and state you want in here to check against.
In your case add [query] as a second para:
useEffect(() => {
fetchResults();
console.log("use effect triggered")
},[query])
https://css-tricks.com/run-useeffect-only-once/

CoinGecko API data undefined after re-render React JS

Trying to render data from the CoinGekco API in my React component. It works on first render but if I leave the page or refresh, coin.market_data is undefined. I also tried passing coin to the useEffect() dependency array and that didn't work.
import React, { useEffect, useState } from "react";
import axios from "../utils/axios";
import CoinDetail from "./CoinDetail";
function CoinPagePage() {
const [coin, setCoin] = useState({});
useEffect(() => {
const getCoin = () => {
const coinid = window.location.pathname.split("/").splice(2).toString();
axios
.get(`/coins/${coinid}`)
.then((res) => {
setCoin(res.data);
console.log(res.data);
})
.catch((error) => console.log(error));
};
getCoin();
}, []);
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
}
export default CoinPagePage;
The GET request only happens when rendering the parent page. Re-rendering the child component will not run the fetch code again. Instead of passing current_price as a prop to your <CoinDetail> component, you could try passing coinid and doing the fetch inside your detail page.
That way, when the page is refreshed, the request will be executed again.
Edit
If you try to access a not existing property on an object, your application will crash. What you could do to prevent this from happening is checking if the request is done, before trying to access the property.
One way you could do this by setting the initial state value to null
const [coin, setCoin] = useState(null);
Then, above the main return, you could check if the value is null, if it is, return some sort of loading screen
if(coin === null) return <LoadingScreen />;
// main render
return (
<div>
<CoinDetail current_price={coin.market_data.current_price.usd} />
</div>
);
This way, when the fetch is done, the state gets updated and the page will re-render and show the updated content.

Filter in react query not working properly on first attempt

I am trying to get only females from an array using a filter, but on the first attempt react query returns the whole array, after that it is working fine. Any idea what property I have to add or remove, so this side effect disappears.
Here is my code:
import React, { useState } from "react";
import { useQuery } from "react-query";
import getPersonsInfo from "../api/personCalls";
export default function Persons() {
const [persons, setPersons] = useState([]);
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onSuccess: (data) => {
setPersons(data.data);
},
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
const result = personData.data.filter(
(person) => person.gender === "female"
);
setPersons(result);
};
return (
<>
<button onClick={getFemaleOnlyHandler}>Female only</button>
{status === "loading" ? (
<div>Loading ... </div>
) : (
<div>
{persons.map((person) => (
<div>
<p>{person.name}</p>
<p>{person.lastName}</p>
<p>{person.address}</p>
<p>{person.gender}</p>
<p>{person.country}</p>
<p>{person.city}</p>
</div>
))}
</div>
)}
</>
);
}
I added the full code in code sandbox: https://codesandbox.io/s/relaxed-drake-4juxg
I think you are making the mistake of copying data from react-query into local state. The idea is that react-query is the state manager, so the data returned by react-query is really all you need.
What you are experiencing in the codesandbox is probably just refetchOnWindowFocus. So you focus the window and click the button, react-query will do a background update and overwrite your local state. This is a direct result of the "copy" I just mentioned.
What you want to do is really just store the user selection, and calculate everything else on the fly, something like this:
const [femalesOnly, setFemalesOnly] = React.useState(false)
const { data: personData, status } = useQuery("personsData", getPersonsInfo, {
onError: (error) => {
console.log(error);
}
});
const getFemaleOnlyHandler = () => {
setFemalesOnly(true)
};
const persons = femalesOnly ? personData.data.filter(person => person.gender === "female") : personData.data
you can then display whatever you have in persons, which will always be up-to-date, even if a background update yields more persons. If the computation (the filtering) is expensive, you can also use useMemo to memoize it (compute it only when personData or femalesOnly changes - but this is likely a premature optimization.
I'm not totally familiar with react-query however the problem is likely that it is re-fetching (async!) everytime the component updates. Since setPersons() triggers an update (ie. sets state) it'll update the new persons state to be the filtered female list and then trigger a fetch of all persons again which comes back and sets the persons state back to the full list (ie. see what happens when you click the female filter button and then just leave it).
There is a more idiomatic way to achieve this in React which is to keep a "single source of truth" (ie. all the persons) and dynamically filter that based on some local ui state.
For example see below where data becomes the source of truth, and persons is a computed value out of that source of truth. This has the benefit that if your original data changes you don't have to manually (read: imperatively) update it to also be females only. This is the "unidirectional data flow" and "reactivity" people always talk about and, honestly, it's what makes React, React.
const { data = { data: [] }, status } = useQuery(
"personsData",
getPersonsInfo,
{
onSuccess: (data) => {},
onError: (error) => {
console.log(error);
}
}
);
const [doFilterFemale, setFilterFemale] = useState(false);
const persons = doFilterFemale
? data.data.filter((person) => person.gender === "female")
: data.data;
https://codesandbox.io/s/vigorous-nobel-9n117?file=/src/Persons/persons.jsx
This is ofc assuming you are always just loading from a json file. In a real application setting, given a backend you control, I would always recommend implementing filtering, sorting and pagination on the server side otherwise you are forced to over-fetch on the client.

Categories

Resources