I am implementing a e-commerce project in which i want store cartItems in local storage in react so it does not disappear after refresh.
Cart Items are set to local storage successfully but when i refresh it again set to empty array.
here is the code:
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems])
useEffect(() => {
let cartProducts = JSON.parse(localStorage.getItem("cartItems"));
if(cartProducts){
setCart({...cart,cartItems:[...cartProducts]});
}
}, [])
here is the state:
const [cart, setCart] = useState({
cartItems: []
});
On Each refresh of page, due to this stateAssignment
const [cart, setCart] = useState({cartItems: []}); This useEffect is executed.
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems])
And at that time cart.cartItems is []. Hence it is set to [].
You need to make sure that this useEffect should only run when there is a user-initiated change in cart.cartItems.
Issue
When both effects run when the component mounts, the initial render uses the empty array ([]) state to update localStorage. The second effect picks up the newly set empty array and reads it back into state.
Solution
Use an initializer function for your state. Then the effect can persist cart to localStorage upon updates as usual.
const initializeState = () => ({
cartItems: JSON.parse(localStorage.getItem("cartItems")) || [],
});
const [cart, setCart] = useState(initializeState());
useEffect(() => {
localStorage.setItem("cartItems", JSON.stringify(cart.cartItems));
}, [cart.cartItems]);
Related
I use a lot of firestore snapshots in my react native application. I am also using React hooks. The code looks something like this:
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// When the component initially loads, add all the loaded data to state.
// When data changes on firestore, we receive that update here in this
// callback and then update the UI based on current state
});;
}, []);
At first I assumed useState would be the best hook to store and update the UI. However, based on the way my useEffect hook is set up with an empty dependency array, when the snapshot callback gets fired with updated data and I try to modify the current state with the new changes, the current state is undefined. I believe this is because of a closure. I am able to get around it using useRef with a forceUpdate() like so:
const dataRef = useRef(initialData);
const [, updateState] = React.useState();
const forceUpdate = useCallback(() => updateState({}), []);
useEffect(() => {
someFirestoreAPICall().onSnapshot(snapshot => {
// if snapshot data is added
dataRef.current.push(newData)
forceUpdate()
// if snapshot data is updated
dataRef.current.find(e => some condition) = updatedData
forceUpdate()
});;
}, []);
return(
// JSX that uses dataRef.current directly
)
My question is am I doing this correct by using useRef along with a forceUpdate instead of useState in a different way? It doesn't seem right that I'm having to update a useRef hook and call forceUpdate() all over my app. When trying useState I tried adding the state variable to the dependency array but ended up with an infinite loop. I only want the snapshot function to be initialized once and the stateful data in the component to be updated over time as things change on the backend (which fires in the onSnapshot callback).
It would be better if you combine useEffect and useState. UseEffect will setup and detach the listener, useState can just be responsible for the data you need.
const [data, setData] = useState([]);
useEffect(() => {
const unsubscribe = someFirestoreAPICall().onSnapshot(snap => {
const data = snap.docs.map(doc => doc.data())
this.setData(data)
});
//remember to unsubscribe from your realtime listener on unmount or you will create a memory leak
return () => unsubscribe()
}, []);
Then you can just reference "data" from the useState hook in your app.
A simple useEffect worked for me, i don't need to create a helper function or anything of sorts,
useEffect(() => {
const colRef = collection(db, "data")
//real time update
onSnapshot(colRef, (snapshot) => {
snapshot.docs.forEach((doc) => {
setTestData((prev) => [...prev, doc.data()])
// console.log("onsnapshot", doc.data());
})
})
}, [])
I found that inside of the onSnapshot() method I was unable to access state(e.g. if I console.log(state) I would get an empty value.
Creating a helper function worked for, but I'm not sure if this is hack-y solution or not but something like:
[state, setState] = useState([])
stateHelperFunction = () => {
//update state here
setState()
}
firestoreAPICall.onSnapshot(snapshot => {
stateHelperFunction(doc.data())
})
use can get the currentState using callback on set hook
const [state, setState] = useState([]);
firestoreAPICall.onSnapshot(snapshot => {
setState(prevState => { prevState.push(doc.data()) return prevState; })
})
prevState will have Current State Value
I'm trying to figure out why my useEffect hook keeps getting called multiple times, even when the dependency has the same value. I'm using the following code:
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import Cards from '../../../cards/Cards'
import UserCard from '../../../cards/users/Card'
import LoadingContainer from '../../../LoadingContainer'
import UsersResource from '../../../../api/resources/Users'
const Users = ({ users }) => (
<Cards>
{users.map((user) => (
<UserCard user={user} key={`user-${user.id}`} />
))}
</Cards>
)
const UsersPage = () => {
const [initialLoad, setInitialLoad] = useState(true)
const [loading, setLoading] = useState(true)
const [initialUsers, setInitialUsers] = useState([])
const [users, setUsers] = useState([])
const fetchUsers = async () => {
setLoading(true)
const response = await UsersResource.getIndex()
setInitialUsers(response.data)
}
useEffect(() => {
fetchUsers()
}, [])
useEffect(() => {
console.log('users changed:', users)
initialLoad ? setInitialLoad(false) : setLoading(false)
}, [users])
useEffect(() => {
setUsers(initialUsers)
}, [initialUsers])
return (
<LoadingContainer
loading={loading}
hasContent={!!users.length}
>
<Users users={users} />
</LoadingContainer>
)
}
Users.propTypes = {
users: PropTypes.arrayOf(PropTypes.shape).isRequired,
}
export default UsersPage
This is the effect that gets re-run when the value of the users dependency stays the same:
useEffect(() => {
console.log('users changed:', users)
initialLoad ? setInitialLoad(false) : setLoading(false)
}, [users])
Here's the output:
users changed: []
users changed: []
users changed: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
So users is obviously being recognized as changed twice, even though both times the effect is called, it returns the same value. This results in my loading state being set to false before the request finishes.
It only runs once if I change the initial state assignment of users from this...
const [users, setUsers] = useState([])
To this...
const [users, setUsers] = useState(initialUsers)
This tells me that the component must be rerendering simply because users is pointing to initialUsers in the second effect, instead of just a blank array (even though initialUsers returns a blank array as well). Can anyone explain why this happens this way? I can't seem to find any documentation describing this behavior (maybe I'm blind).
I would expect the value to be the only thing to influence an effect, but it seems like it might get triggered because the dependency is pointing to a new reference in memory. Am I off?
This appears to be a bit of a misunderstanding between value equality and reference equality. React uses reference equality.
The initial initialUsers and users state values are [], and on the initial render cycle there is a useEffect hook that enqueues an update to users with the current initialUsers value.
Note that initialUsers isn't not the same reference as users, so initialUsers === users evaluates false.
const initialUsers = [];
const users = [];
console.log(initialUsers === users); // false
Note also that [] === [] is also never true since they are two object references.
console.log([] === []); // false
This is roughly how the logic flows:
On the initial render cycle the initial users state [] is logged in the second useEffect hook.
The useEffect with dependency on initialUsers runs and updates the users state to the value of the initialUsers state. [] (but a different reference).
The second useEffect hook logs the users state update, again [].
The fetchUsers handler has fetched data and enqueues an update to the initialUsers state.
The second useEffect hook logs the users state update, now a populated array.
Code:
const fetchUsers = async () => {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
// (4) update initialUsers
setInitialUsers(response.data);
};
useEffect(() => {
fetchUsers();
}, []);
useEffect(() => {
// (1) initial render, first log "[]"
// (3) second render, second log "[]"
// (5) third render, third log "[.........]"
console.log("users changed:", users);
initialLoad ? setInitialLoad(false) : setLoading(false);
}, [users]);
useEffect(() => {
// (2) initial render update users
setUsers(initialUsers);
}, [initialUsers]);
The difference when you initialize the users state to the initialState value is now they are the same reference.
const initialUsers = [];
const users = initialUsers;
console.log(initialUsers === users); // true
This subtle difference skips the enqueued update #2 above since users and initialUsers are already the same reference.
I have this hook
const [folders, setFolders] = useState([]);
How can I local store this data so when you refresh the page data in useState saves
you might store the data in the localStorage, but when you use the setFolders() function, you must update it in localStorage.
Your code should look like this:
const [folders, setFolders] = useState(JSON.parse(localStorage.getItem('folders')) || [])
const setFoldersStorage = (new) => { setFolders(new); localStorage.setItem('folders', JSON.stringify(new)); }
when you want to update folders, you use the setFoldersStorage()
So mainly, what you need is to set an item to the local storage every time the folders state changes and at the same time update setFolders every time the component re-renders.
function MyComponent(){
const [folders, setFolders] = useState([])
// update setFolders with the local storage data
useEffect(() => {
setFolders(JSON.parse(window.localStorage.getItem('folders')))
}, [])
// save folders state in local storage when it changes
useEffect(() => {
window.localStorage.setItem('folders', folders)
}, [folders])
return <>{folders}</>
}
// Retrieve the initial data from the localStorage
const [folders, setFolders] = useState(
() => JSON.parse(localStorage.getItem("folders") || "[]"));
// store the value into the localStorage whenever folders is updated
useEffect(() => {
localStorage.setItem("folders", JSON.stringify(folders));
}, [folders]);
You can also create a custom hook so you can reuse it
function useStateLs(initValue, lsKey) {
const [value, setValue] = useState(
() => JSON.parse(localStorage.getItem(lsKey) || JSON.stringify(initValue))
);
useEffect(() => {
localStorage.setItem(lsKey, JSON.stringify(value));
}, [value]);
return [value, setValue];
}
const [folders, setFolders] = useStateLs([], "folders");
I'm working on a navbar that changes whenever it detects there is a user in local storage using useState and useEffect. Here's my logic:
const [user, setUser] = useState("")
function fetchData(){
const item = JSON.parse(localStorage.getItem('name'))
if(item) {
setUser(item)
}
}
useEffect(() => {
fetchData()
});
return({user? (<LoggedIn />) : (<ClientBar />})
The code essentially begins with no user, and in the fetchData() function it checks whether a user exists in local storage and sets the user based on what's been found in local storage. I can tell the logic works because when I refresh it, it changes from <ClientBar> to <LoggedIn>. However, the problem is it doesn't work upon login - rather, it requires a refresh to update. Is there a way to make it update immediately upon login?
Your useEffect should be:
useEffect(() => {
fetchData()
}, [user])
So when user changes it will check each time. Because you're fetching localstorage I'd also encourage you to add a loading component while that's being done.
Checking localstorage Should also be using async/await:
const fetchData = async () => {
const item = await JSON.parse(localStorage.getItem('name'))
if(item) setUser(item)
}
While this it's waiting for the check you should render a loading with another useState.
As Abhi Patil stated, you need to check localStorage changes. It has nothing to do with useEffect as it is only triggered when the component is mounted.
You need to wrap fetchData() inside storage event listener like so:
useEffect(() => {
window.addEventListener('storage', () => {
const item = JSON.parse(localStorage.getItem('name'))
if(item) {
setUser(item)
}
})
})
I work on a todo app with React and things become clearer, but I struggle to undersand the "lifecycle". In VueJS I know a ComponentDidMount() hook, which would help me to solve this issue if I guess, but in React I can´t find it out.
I have an array of todos like this: const todos = [{description: "walk dog", done: false}]
This is the initial state of my app:
const [alltodos, handleTodos] = useState([]);
On load I use this useEffect hook to get data from localStorage.
useEffect(() => {
const items = localStorage.getItem("todos");
const parsed = JSON.parse(items);
handleTodos(parsed);
}, []);
I count my todos with this function:
const countTodos = () => {
const donetodos = alltodos.filter((item) => {
return !item.done;
});
countOpen(donetodos.length);
};
I update the count if a dependency changes:
useEffect(() => {
countTodos();
localStorage.setItem("todos", JSON.stringify(alltodos));
}, [alltodos]);
So what happens is that the counter starts with 0 and than "flickers" for a milisecond before it shows the number of todos which I get from localstorage.
Is there a way to prevent that behaviour? As far as I know the component gets rendered FIRST and then the useEffect hook gets triggered. How I render my component AFTER the data is pulled from localstorage?
The best way to do this would be with a lazy initial state. Also, cleaning up the variables and using a standardized [variable, setVariable] will save you headache debugging in the future.
const [alltodos, setAlltodos] = useState(() => {
const items = localStorage.getItem("todos");
const parsed = JSON.parse(items);
return parsed || "";
});
Initialize the allTodos state with null. As long as this state is null, render a notification or just return null to render nothing.
You can calculate the open todos count directly from the current alltodos state, without the need of useEffect.
const [alltodos, handleTodos] = useState(null);
useEffect(() => {
const items = localStorage.getItem("todos");
const parsed = items ? JSON.parse(items) : [];
handleTodos(parsed);
}, []);
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(alltodos));
}, [alltodos]);
if(alltodos === null) return 'Loading todos list';
// this is derived from state, so you don't have to create a state for it
const openTodosCount = alltodos.reduce((acc, o) => acc + !o.done, 0);
The fastest way would be to add new state that would be responsible for loading.
For example
const [isLoading, setIsLoading] = useState(true);
set it initially on true and then after you do all your calculations change it to false.
Then you can depend on that state and show div with a text "Loading" or anything else but when isLoading would go to false it will show component elements.