Setting state array in react - arrays

I have been trying to push an object into a state array and then into local storage so it can remain visible even after refreshing, everything works fine except final push doesent happen. In order for an object to stay saved in local i have to add it and one after it, and the one after dont get saved. Any help is much appreciated
function App() {
const [data, setData] = useState([{ name: "", thumbnail: { path: "" } }]);
const [team, setTeam] = useState(JSON.parse(localStorage.getItem("team")));
console.log(team);
useEffect(() => {
fetch(
"http://gateway.marvel.com/v1/public/characters?apikey="
)
.then((data) => data.json())
.then((data) => setData(data.data.results));
}, []);
const addToTeam = (hero) => {
!team ? setTeam([hero]) : setTeam([...team, hero]);
localStorage.setItem("team", JSON.stringify(team));
};

React state updates are not synchronous.
So when you run this code:
const addToTeam = (hero) => {
!team ? setTeam([hero]) : setTeam([...team, hero]);
localStorage.setItem("team", JSON.stringify(team));
};
you could be setting previous value of team instead of the value you just set.
To fix the problem you can make a side effect that runs when the team state changes and update localStorage from it.
useEffect(() => {
localStorage.setItem("team", JSON.stringify(team));
}, [team]);

Related

localStorage not properly saving array in React

Working on a forum and I need my topics to start not erasing upon refresh so that I can start checking whether or not the messages display properly.
I have it so each time I make a topic, the topic is stored as an object inside an array. Each object stores different types of information like the title of the topic, the message, the author and the date. The array is then sorted and mapped and displays all the information on the page.
The addTopic function is used as an onClick for a form that pops up.
I have my localStorage set up using a useEffect like it was suggested however every time I make a topic and refresh the page, the array still erases itself and I'm back to the original state. Please advice.
const [topic, setTopic] = useState([]);
const [title, setTitle] = useState();
const [message, setMessage] = useState();
const addTopic = (e) => {
e.preventDefault();
const updatedTopic = [
...topic,
{
title: title,
message,
author: "Dagger",
date: new Date(),
},
];
setTopic(updatedTopic);
};
useEffect(() => {
localStorage.setItem("topic", JSON.stringify(topic));
}, [topic]);
useEffect(() => {
const topics = JSON.parse(localStorage.getItem("topic"));
if (topics) {
setTopic(topic);
}
}, []);
Effects run in order, so when you refresh the page the code you have here is going to setItem before you getItem.
An alternative to your second useEffect there is to initialise your useState hook directly from the localStorage:
const [topic, setTopic] = useState(
() => {
const topicJson = localStorage.getItem("topic");
return topicJson ? JSON.parse(topicJson) : [];
});
Yes the reason is JSON.parse(localStorage.getItem("topic")) gives you the string "undefined" instead of the object undefined.
Instead try doing
useEffect(() => {
let topics = localStorage.getItem("topic");
if (topics) {
topics = JSON.parse(topics)
setTopic(topics);
}
}, []);

React useState is not updating when set method is called in an interval

I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilĂ 
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);

How to use useEffect correctly to apply modifications to the array state using Firestore?

I'm using React firebase to make a Slack like chat app. I am listening to the change of the state inside the useEffect on rendering. (dependency is []).
The problem I have here is, how to fire the changes when onSnapshot listener splits out the changed state. If change.type is "modified", I use modifyCandidate (which is an interim state) to save what's been updated, and hook this state in the second useEffect.
The problem of second effect is, without dependency of chats, which is the array of chat, there is no chat in chats (which is obviously true in the initial rendering). To get chats, I add another dependency to second effect. Now, other problem I get is whenever I face changes or addition to the database, the second effect is fired even if modification didn't take place.
How can I effectively execute second effect when only modification occurs as well as being able to track the changes of chats(from the beginning) or
am I doing something awkward in the listening phase?
Please share your thoughts! (:
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New message: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified message: ", change.doc.data());
setModifyCandidate(change.doc.data());
}
if (change.type === "removed") {
console.log("remove message: ", change.doc.data());
}
});
});
}, [])
useEffect(() => {
if(!modifyCandidate){
return
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
setChats(copied)
}, [modifyCandidate, chats])
initially, I also use this useEffect to load chats.
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").get().then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setChats(data);
})
}, [])
return <>
{
chats.map((chat) => {
return <div key={chat.id}>
<ChatCard chat={chat} users={users} uid={uid} index={chat.id} onEmojiClick={onEmojiClick}/>
</div>
})
}
</>
use useMemo instead of 2nd useEffect.
const chat = useMemo(() => {
if(!modifyCandidate){
return null
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
return copied
}, [modifyCandidate])

React hooks : how to watch changes in a JS class object?

I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox

react useState enters infinite loop even thou variable is the same

I have the following code:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, [ddFilterData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}
What I need is for useEffect to execute eah time ddFilerData changes with NEW or DIFFERENT data.
From my understanding it should only update or run when teh ddFilterData is different no?
Currently it runs on each change. The code above enters into an infinite loop even thou filteredData isn't different. Any ideas what I'm doing wrong?
Your returnedData is an array. So when you do setddFilterData(returnedData) you're setting a new value for ddFilterData. Because React uses Object.is for comparison, even if the array elements are the same as previously, it is still a different object and will trigger useEffect again, causing the infinite loop.
your getDropdownData method is updating ddFilterData which causes re-render. And on re-render you getDropdownData is called which updated ddFilterData due to this cyclic behavior your are getting infinte loop.
Modify your code like this:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, []);
useEffect(() => {
// put your code here if you want to do something on change of ddFilterData
}, [getDropdownData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}

Resources