I've started to learn web development recently and this is making my head hurt. I made a simple todo app where you can add an item with id and value to an array and delete the item from the array. Now I want whenever the site refreshes, I want the items to be there. I made a testing array and for some reason, when refreshed, it works but not with data.
const [items, setItems] = useState([]);
const testarr = [{id: 1, value: "a"}, {id: 2, value: "b"}];
useEffect(() => {
const data = JSON.parse(localStorage.getItem("items"));
console.log(data);
// setItems(testarr);
setItems(data);
}, [])
useEffect(() => {
window.localStorage.setItem("items", JSON.stringify(items));
}, [items]);
What I came up with:
useEffect(() => {
const data = JSON.parse(localStorage.getItem("items"));
data.forEach(element => {
setItems(oldarr => [...oldarr, element]);
});
}, [])
The problem is, if I keep refreshing the site fast enough, eventually the array will become empty again. I don't understand, when refreshed it logs data just fine but when I want to setItems(data) that doesn't work. Does anyone know why it behaves like this?
setState is an asynchronous function, this implies that this code snippet below will always run with items as empty arrays before items receive data from "setItems(data)".
useEffect(() => {
window.localStorage.setItem("items", JSON.stringify(items));
}, [items]);
maybe, you can check if items isn't a empty array before store
useEffect(() => {
if (Array.isArray(items) && items.length > 0) {
window.localStorage.setItem("items", JSON.stringify(items));
}
}, [items]);
Please try localStorage without window Object
For example:
useEffect(() => {
localStorage.setItem("items", JSON.stringify(items));
}, [items]);
Issue
The below useEffect runs every time items changes, but also on mount. And on mount items is equal to [], that array given to useState in the beginning. That's why the localStorage is being set to [].
useEffect(() => {
window.localStorage.setItem("items", JSON.stringify(items));
}, [items]);
Solution
One way to solve this issue is to change your state definition as below, so when there is data in the localStorage you pick it:
const [items, setItems] = useState(!localStorage.getItem("items") ? [] : JSON.parse(localStorage.getItem("items")));
And at this point, you can remove those two useEffects you have there for getting data from the localStorage and giving it to setItems.
Related
So I have an array with the following structure:
`
export const transacciones = [
{
id:100,
cantidad: 0,
concepto : 'Ejemplo',
descripcion: 'Ejemplo',
},
]
`
This array will dynamically increase or decrease as I push or filter items in it (Exactly like data in a task list)
The problem is that I am trying to add some data persistence using local storage. I guess data is getting stored but not shown when I refresh my browser (chrome).
However, when I refresh data disappears from where it was in the upper image so I`m not even sure if I am correctly storing it.
I've tried two things using useEffect hooks.
First aproach:
`
const [transacciones,setTransacciones] = useState([]);
useEffect(()=>{
localStorage.setItem('transacciones',JSON.stringify(transacciones))
},[transacciones])
useEffect(() =>{
const transacciones = JSON.parse(localStorage.getItem('transacciones'))
if (transacciones){
setTransacciones(transacciones)
}
},[])
`
I read somewhere that as the initial value of use state is [] I should chage things in there, so...
Second aproach:
`
const [transacciones,setTransacciones] = useState([],()=>{
const localData = localStorage.getItem('transacciones');
return localData ? JSON.parse(localData) : [];
});
useEffect(()=>{
localStorage.setItem('transacciones',JSON.stringify(transacciones))
},[transacciones])
`
However, when I refresh I get the same result: No persistence.
What am I missing here? Any help would be appreciated
In both scenarios your transacciones array is empty when you perform the localStorage.setItem. if you're trying to keep your local state sync with localStorage this might help:
export function useTransacciones(initialValue){
const localData = localStorage.getItem('transacciones');
const [transacciones,_setTransacciones] = useState(localData?JSON.parse(localData) : initialValue); // you can choose your own strategy to handle `initialValue` and cachedValue
const setTransacciones = (data) => {
_setTransacciones(data)
localStorage.setItem(JSON.stringify(data))
}
hydrate(){
const data = localStorage.getItem("transacciones")
setTransacciones(JSON.prase(data))
}
return [ transacciones, setTransacciones, hydrate ]
}
which you can use it anywhere with caching compelexity hidden inside:
const [transacciones, setTransacciones] = useTransacciones([])
It's a next.js app and I populate the data using a useSWR hook.
const { data, error } = useSWR('/api/digest', fetcher, {
revalidateOnFocus: false,
})
The issue is that the DOM doesn't get updated as expected after the mutate() line below. But if I hard code the data as updatedData and pass it as the arg for the mutate it works normally. The fact is that data and the updatedData are the same. See comments below.
Edit: If I click on any Navbar <Link/> it gets updated.
Any clues of what is happening?
const handleAddRowClick = async () => {
const newRow = { category: selectedCategory, entity: selectedEntity }
data.subscription[selectedCategory].push(selectedEntity);
console.log(data); // This data is equal to updatedData
const updatedData = {
"subscription": {
"materialFact": [
"Boeing"
],
"headlines": [
"thejournal",
"onemagazine" // ***This is the value added.
]
}
}
// mutate('/api/digest', data, false) // This does not works.
mutate('/api/digest', updatedData , false) // It works.
}
I am assuming that the data property you use in handleAddRowClick is the same that you get from useSWR. Now, if you update some deeply nested object in data, it doesn't change the data reference. It still points to the old object. So, if you pass it again to mutate, for mutate, the object is still the same and hence, it won't trigger a re-render.
I would recommend that you do something like the following to derive updatedData from data and then pass it to the mutate function.
const handleAddRowClick = async () => {
const updatedData = {
...data,
subscription: {
...data.subscription,
[selectedCategory]: [
...data.subscription[selectedCategory],
selectedEntity,
]
}
}
mutate('/api/digest', updatedData , false);
}
On a side note, you can also use something like immer to simplify copying the state.
I am currently building a clothing shop in which you can add products to the cart, and delete each one of them as you like and it makes the cart re-render and display a new cart without that product within it.
so I've made a Slice for cart in redux. the 'addProduct' part works fine, but the 'deleteProduct' reducer which uses filter() doesn't work. (when i click my delete button nothing happens, and no changes in difference in Redux Devtools)
my slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
my button and handleDelete:
<button className="btn-delete" onClick={handleDelete(product)}>Delete</button>
function handleDelete(p) {
console.log(p)
dispatch(deleteProduct(p.product))
}
edit:
filter didnt work in every possible way i tried. so i changed my way and did this instead to work. but still i wonder why didnt filter() method work properly.
deleteProduct: (state, { payload }) => {
const foundIndex = state.productList.findIndex((p) => {
return p.product.id === payload.id
})
state.productList.splice(foundIndex,1)
}
The filter operation creates a new array without changing the existing instance. Therefore you need to assign it to state.
deleteProduct: (state, { payload }) => {
return {
...state,
productList: state.productList.filter(
foundProduct => foundProduct.id !== payload.id
)
};
};
You also need to change the way you call the handleDelete in the onClick handler.
onClick={() => handleDelete(product)}
Filter methods returns new array so you have to update your existing array tool. Please update your slice:
const selectedProductsSlice = createSlice({
name:'selectedProducts',
initialState:{
productList:[],
checkoutPrice:0,
},
reducers: {
addProduct:(state,{payload}) => {
state.productList.push(payload)
state.checkoutPrice += payload.price
},
deleteProduct:(state,{payload}) => {
state.productList=state.productList.filter((foundProduct) => foundProduct.id !== payload.id)
}
}});
I went through exactly same problem as you.
I solved this problem just to move my reducer to another existing reducer
Hope you to get through it. I couldn't pass it by
I have a list of data like this:
data = [
{
id:1,
name: "boo"
},
{
id:1,
name: "boo"
}
]
I initialize it with react UseState like follows:
const [items, setItems] = useState(data)
And I map this in my react code and visualize them.
If I want to delete one I do it with the following function:
const deleteItem = async (id) => {
console.log('BEFORE- - - ->', items);
// this functions calls an api to delete the item
await deleteItem({ id });
setItems((oldItems) =>
oldItems.filter(({ id }) => id !== commentId)
);
console.log('AFTER- - - ->', items);
};
Graphically the component with the deleted item disappears, but actually the second console.log prints the same thing as the first log. In this case if I delete the element with id:1 I will get the same log while I see graphically that the element disappears. This causes a problem with indexing if I want to delete more than one item.
Which do you think is the problem? has I to do it into one useEffect?
Setting state is an asynchronous action. If you put a console outside of that function, on the next re-render you will see your state updated correctly (just like you've seen your item disappear visually).
This happens because setState is asynchronous so the state really changes after you console.log
I got a situation where I do not have the experience to know which method is the best and what im doing wrong. The situation is as following:
I got a page with products which have a input + order button, which will add the order to the shoppingcart. My thought was to first set the state for each order you make:
const [amountItem, setAmountItem] = useState({
product: {
id: '',
amount: ''
}
});
Updating:
function handleChange(evt, id) {
const value = evt.currentTarget.value;
setAmountItem({
...amountItem,
product:{
id: id,
amount: value
}
});
console.log(amountItem);
}
Which then I push to the shoppingcart/checkout page (no modal):
if (e.target[0].value < productItem.stock) {
history.push({
pathname: `/winkelwagen/`,
state: {data: amountItem}
});
On this page, i first check if location.state exists before using the shoppingcart component:
if (location.state !== null && shoppingCartItems === '') {
console.log(location.state.data);
setShoppingCartItems(location.state.data);
setShoppingCartActive(true);
let cartString = JSON.stringify(shoppingCartItems);
localStorage.setItem('shopping_carts', cartString)
}
When it does exist, some product is ordered with an amount and must be set to localstorage, the product is 'always' visible when refreshing, etc. Until this point it works, the localstorage item exists:
(key)shopping_carts (value){"product":{"id":3,"amount":"2"}}
After that comes the shoppingcart component:
<ShoppingCart
shoppingCartItems={shoppingCartItems}
setShoppingCartItems={setShoppingCartItems}
shoppingCartActive={shoppingCartActive}
setShoppingCartActive={setShoppingCartActive}
/>
This is where my problem starts. Long story short, it only shows the single item from the state, which obviously will be gone.
In this file I got a useEffect part for the localstorage:
useEffect(() =>{
let shoppingCart = localStorage.getItem("shopping_carts");
console.log('shoppingcartitems ');
shoppingCart = JSON.parse(shoppingCart);
console.log(shoppingCart);
if (shoppingCart !== "") {
const id = shoppingCartItems.id;
const amount = shoppingCartItems.amount;
//setShoppingCartItems(shoppingCart)
setShoppingCartItems(prevState => ({
...prevState,
product: {
...shoppingCartItems,
id: id,
amount: amount
}
}))
}
}, [setShoppingCartItems])
The output for 'shoppingCart' is <empty string>. Why is that? Is the format wrong? I'm also using the localstorage for other info, which works fine. I know the setShoppingCartItems is not correct for multiple values, but I wanted to test this single entry first.
Update:
const CheckoutPage = () => {
const location = useLocation();
const [shoppingCartItems, setShoppingCartItems] = useState('');
const [shoppingCartActive, setShoppingCartActive] = useState(false);
const [mode, setMode] = useState('init');
let savedShoppingCart = JSON.parse(localStorage.getItem("shopping_carts"));
console.log('saved shopping cart: ')
console.log(savedShoppingCart);
if (savedShoppingCart !== "" && mode === 'init') {
const id = savedShoppingCart.id;
const amount = savedShoppingCart.amount;
//setShoppingCartItems(shoppingCart)
setShoppingCartItems(prevState => ({
...prevState,
product: {
...shoppingCartItems,
id: id,
amount: amount
}
}))
setMode('data');
//setShoppingCartActive(true);
}
if (location.state !== null && shoppingCartItems === '') {
console.log(location.state.data);
setShoppingCartItems(location.state.data);
setShoppingCartActive(true);
let cartString = JSON.stringify(shoppingCartItems);
localStorage.setItem('shopping_carts', cartString)
}
return (
<div className="shoppingCartPage">
<ShoppingCart
shoppingCartItems={shoppingCartItems}
setShoppingCartItems={setShoppingCartItems}
shoppingCartActive={shoppingCartActive}
setShoppingCartActive={setShoppingCartActive}
/>
</div>
)
}
So basically I want to do 3 things here:
Get the data from the localstorage item
Is there a saved localstorage item? Add it to existing shoppingCartItems (prevstate)
Save the updated (or new when no localstorage item exists) shoppingCartItems after that
After that I want to pass the data to the shoppingcart where i can increase/decrease items or remove/splice the values.
Treat useEffect with caution as an eventListener on React state.
Therefore you need to specify in the dependency array everything might change, in order to trigger the useEffect callback.
In your useEffect dependencies, where you are updating your shoppingCartItems, you have added only setShoppingCartItems - which I assume that its a setState function. This results in your useEffect te be called only once at the app start because setState functions never change.
So, to have your shoppingCartItems updated via useEffect you need to add it to dependencies.
useEffect(() => {
// your code
}, [setShoppingCartItems, shoppingCartItems])
This may fix your problem, because you never call logic that saves update shopping cart state, the second time, therefore you get empty in your console log.