localStorage not properly saving array in React - reactjs

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);
}
}, []);

Related

how can I transfer the data to another component (useEffect problem)

In useEffect, I retrieve the data from the server and store it in the "products" array:
const { url } = props;
const [products, setProducts] = useState([]);
useEffect(() => {
const fetchProducts = async () => {
setLoadingSpinner(true);
const response = await fetch(url);
const responseData = await response.json();
setLoadingSpinner(false);
const loadedProducts = [];
for (const key in responseData) {
loadedProducts.push({
id: key,
name: responseData[key].name,
description: responseData[key].description,
price: responseData[key].price,
image: responseData[key].image,
processor: responseData[key].processor,
});
}
setProducts(loadedProducts);
setIsDataLoaded(true);
};
fetchProducts();
}, [url, isDataLoaded]);
I pass it to the ProductDetail component:
<ProductDetail products={products}></ProductDetail>
I always get a null value in the ProductDetail component:
function ProductDetail(props) {
const params = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
if (props.products && params.productId) {
const product = props.products.find(product => product.id === params.productId);
setProduct(product);
}
}, [props.products, params.productId]);
console.log(product)
I realized that when useEffect is run for the first time, my "products" array is still empty and it sends it to the component, so it's not good. But I don't know how to fix this.
There are a couple of things wrong here. I'm going to start with something that seems tangential at first but it will lead to a better answer.
In ProductDetail you introduce a new state called product. You then, in a useEffect, find the product from the list & product id in props and set this back into the state item. This would be unnecessary state duplication (even if not copying a prop verbatim into state, a value derived directly from props still counts) and is going to increase the surface areas for bugs in your application (this is probably the most common beginner's error).
You actually don't need that state item, you just need useMemo:
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
console.log(product)
To solve the issue with the product not being found briefly whilst then products load you can either (a) conditionally render the component so this code doesn't even run in the first place when products isn't fetched yet. Or (b) Change the ProductDetail so that it effectively does nothing until the product is found.
Solution A
const [products, setProducts] = useState(null); // We change the default to null to be able to distinguish empty list case from loading case.
// ...
{products !== null && <ProductDetail products={products}></ProductDetail>}
Solution B
function ProductDetail(props) {
const params = useParams();
const product = useMemo(() => {
if (props.products && params.productId) {
return props.products.find(product => product.id === params.productId);
}
}, [props.products, params.productId]);
if (!product) return null // Return early (do nothing) whilst product is not found
// ... your original render contents

State not updating inside async function in useEffect

Expected:
Following useEffect fetches a list of pool addresses when first rendered and assigns it to getPoolsList, which should then be used to set poolsList state.
const [poolsList, setPoolsList] = useState([]);
useEffect(() => {
async function fetchPools() {
const getPoolsList = await discountmain.methods.allPool().call();
console.log(getPoolsList); //returns ['0x...']
setPoolsList(getPoolsList);
}
fetchPools();
}, []);
The following code I added to check the value of poolsList whenever its value changes.
useEffect(() => {
console.log("useeffect", poolsList); // returns []
}, [poolsList]);
However, poolsList is showing [].
The file is https://codeshare.io/j0wBLD.
What am I doing wrong? I'm a newbie in React.
Your code looks fine, but React can be finicky with arrays in the dependency array
First thing is to actually make sure that the data changes and the useEffect hook is not being called
Here are few things you can try:
Probably the best solution: useReducer
Use the array length and watch the length
const [poolsList, setPoolsList] = useState([]);
const [poolsListLength, setPoolListLength] = useState(0);
useEffect(() => {
async function fetchPools() {
const getPoolsList = await discountmain.methods.allPool().call();
console.log(getPoolsList); //returns ['0x...']
setPoolsList(getPoolsList);
setPoolListLength(getPoolList.length)
}
fetchPools();
}, []);
then
useEffect(() => {
console.log("useeffect", poolsList); // returns []
}, [poolsListLength]);
Use Set
Not my favorite, but you can Stringify the array
useEffect(() => {
console.log(outcomes)
}, [JSON.stringify(outcomes)])

How to fetch data from all documents in a subcollection in firebase firestore using React?

For my project, I want to fetch data from all documents in a subcollection. And there are multiple documents with this subcollection.
To clarify, this is how my firestore is strctured:
I have an events collection which contains multiple documents with doc.id being the event name itself. Each event document has several fields and an attendee subcollection. In the attendee subcollection, each document contains details about the attendee.
I want to map through all documents in the events collection and fetch data about attendees from all of them.
And I want to display this data when the component first renders. So I'm calling the function inside useEffect. Here's what I have tried:
const [attendeeInfo, setAttendeeInfo] = useState({});
const [events, setEvents] = useState([]);
const getEventsData = async () => {
// first of all, we need the name of the user's org
// fetch it from users collection by using the uid from auth
const orgRef = doc(db, "users", auth["user"].uid);
const orgSnap = await getDoc(orgRef);
// now that we have the org name, use that to search events...
// ...created by that org in events collection
const eventsRef = collection(db, "events");
const eventsQuery = query(eventsRef, where("createdBy", "==", orgSnap.data().orgName));
const eventsQuerySnapshot = await getDocs(eventsQuery);
let eventsInfo = [];
eventsQuerySnapshot.forEach((doc) => {
eventsInfo.push(doc.id);
})
setOrg(orgSnap.data().orgName);
setEvents(eventsInfo);
}
const getAttendeesData = (events) => {
console.log(events);
let attendeeInformation = [];
events.forEach(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
})
// console.log(attendeeInformation);
setAttendeeInfo(attendeeInformation);
}
useEffect(() => {
getEventsData();
// console.log(attendeeInfo);
getAttendeesData(events);
}, []);
However, when I console log the events inside my attendeesData function, I get an empty array which means that the events state variable hasn't been updated from previous function.
Can anyone help me solve this?
This is a timing issue. On first render you start fetching the list of events, but you aren't waiting for them to be retrieved before using them. Furthermore, because you only run this code on mount, when events is eventually updated, getAttendeesData won't be invoked with the updated array.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
getAttendeesData(events); // <-- starts fetching attendees for `events`, which will still be an empty array
}, []); // <-- [] means run this code once, only when mounted
The solution to this is to split up the useEffect so each part is handled properly.
useEffect(() => {
getEventsData(); // <-- queues and starts "fetch event IDs" action
}, []); // <-- [] means run this code once, only when mounted
useEffect(() => {
getAttendeesData(events); // initially fetches attendees for an empty array, but is then called again when `events` is updated with data
}, [events]); // <-- [events] means run this code, when mounted or when `events` changes
Next, you need to fix up getAttendeesData as it has a similar issue where it will end up calling setAttendeeInfo() at the end of it with another empty array (attendeeInformation) because you aren't waiting for it to be filled with data first. While this array will eventually fill with data correctly, when it does, it won't trigger a rerender to actually show that data.
const [attendeeInfo, setAttendeeInfo] = useState([]); // <-- should be an array not an object?
const [events, setEvents] = useState([]);
const getAttendeesData = async (events) => {
console.log(events);
const fetchAttendeesPromises = events.map(async (event) => {
const attendeesRef = collection(db, "events", event, "attendees");
const attendeesSnap = await getDocs(attendeesRef);
const attendeeInformation = [];
attendeesSnap.forEach((doc) => {
const isItMentor = doc.data().isMentor ? "Yes" : "No";
const isItMentee = doc.data().isMentee ? "Yes" : "No";
const attendeeData = {
name: doc.id,
mentor: isItMentor,
mentee: isItMentee,
};
attendeeInformation.push(attendeeData);
})
return attendeeInformation; // also consider { event, attendeeInformation }
})
// wait for all attendees to be fetched first!
const attendeesForAllEvents = await Promises.all(fetchAttendeesPromises)
.then(attendeeGroups => attendeeGroups.flat()); // and flatten to one array
// console.log(attendeesForAllEvents);
setAttendeeInfo(attendeesForAllEvents);
}
Applying these changes in a basic and incomplete (see below) way, gives:
// place these outside your component, they don't need to be recreated on each render
const getEventsData = async () => { /* ... */ }
const getAttendeesData = async (events) => { /* ... */ }
export const YourComponent = (props) => {
const [attendeeInfo, setAttendeeInfo] = useState(null); // use null to signal "not yet loaded"
const [events, setEvents] = useState(null); // use null to signal "not yet loaded"
const loading = events === null || attendeeInfo === null;
useEffect(() => {
getEventsData();
}, []);
useEffect(() => {
if (events !== null) // only call when data available
getAttendeesData(events);
}, [events]);
// hide component until ready
// consider rendering a spinner/throbber here while loading
if (loading)
return null;
return (
/* render content here */
)
}
Because getEventsData() and getAttendeesData() are Promises, you should make use of useAsyncEffect implmentations like #react-hook/async and use-async-effect so you can handle any intermediate states like loading, improper authentication, unmounting before finishing, and other errors (which are all not covered in the above snippet). This thread contains more details on this topic.

Setting state array in react

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]);

Trying to map over state in useEffect function, but it mutates the state

I have got the users listing by using redux-thunk, then I mapStateToProps those users to get them inside my props. But after that when I try to map over that props.users.users it changes my state (i.e. props.users.users gets changed even though I have used spread operator for the on that state). Please help with this.
const [users, setUsers] = useState([]);
useEffect(() => {
props.getUsers();
}, []);
useEffect(() => {
let tempUsers = [...props.users.users];
tempUsers = tempUsers.map(user => {
user.created = new Date(parseInt(user.created) * 1000).toString()
user.action = { ...user };
return user;
});
setUsers(tempUsers);
}, [props.users.users]);
You are still mutating the user objects in map. You are directly setting the properties of user object which are still referring to the original user object. You shoul create a new copy of user object too to set its properties.
tempUsers = tempUsers.map(usr => {
let user = { ...usr };
user.created = new Date(parseInt(user.created) * 1000).toString()
user.action = { ...user };
return user;
});

Resources