Why is localStorage getting cleared whenever I refresh the page? - reactjs

Like the title says, the localStorage I set registers the changes made to the todoList array and JSON.stringifys it; however, whenever I refresh the page the array returns to the default [] state.
const LOCAL_STORAGE_KEY = "task-list"
function TodoList() {
const [todoList, setTodoList] = useState([]);
useEffect(() => {
const storedList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedList) {
setTodoList(storedList);
}
}, []);
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
}, [todoList]);

When you reload the app/component both effects will run, and React state updates are processed asynchronously, so it's picking up the empty array state persisted to localStorage before the state update is processed. Just read from localStorage directly when setting the initial todoList state value.
Example:
const LOCAL_STORAGE_KEY = "task-list"
function TodoList() {
const [todoList, setTodoList] = useState(() => {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []
});
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList));
}, [todoList]);
...

The above solution does not work in all cases. Instead add a conditional in front of the localStorage.setItem line in order to prevent the [] case.
//does not work in all cases (such as localhost)
function TodoList() {
const [todoList, setTodoList] = useState(() => {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || []
});
//use conditional instead
useEffect(() => {
if (todoList.length > 0) {localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todoList))}
}, [todoList])

Your React version is above v18 which implemented the <React.StrictMode>. If this is enabled in the index.js this code
useEffect(() => {
const storedList = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedList) {
setTodoList(storedList);
}
}, []);
Won't work because it detects potential problems. If you removed <React.StrictMode> it will work but I won't recommend it. The best solution is the first two answers

Related

ReactJS - use localStorage as a dependency for useEffect causes infinite loop

This code give me infinite loop at line console.log
const userInfo = JSON.parse(localStorage.getItem("user_info"));
const [filterSemester, setFilterSemester] = useState(SEMESTERS[0]);
const [scoreData, setScoreData] = useState(null);
useEffect(() => {
getData();
}, [userInfo, filterSemester]);
useEffect(() => {
console.log("scoreData: ", scoreData);
}, [scoreData]);
const getData = () => {
const params = {
student_id: userInfo?.student_info?.id,
school_year_id:
userInfo?.student_info?.class_info?.grade_info?.school_year_id,
semester: filterSemester.key,
};
getStudyInfoBySchoolYear(params).then((res) => {
if (res?.status === 200) {
setScoreData(res?.data?.data);
}
});
};
If I remove userInfo from the dependency array of the first useEffect, the loop will gone, I wonder why? I didn't change it at all in the code.
userInfo is actually changing.
It is a functional component, so all the code that is inside the component will run on every render, thus, userInfo gets re-created on every render, because it was not declared as a reference (with useRef) or, more commonly, as a state (with useState).
The flow is as follows:
The component mounts.
The first useEffect runs getData. The second useEffect also runs.
getData will update scoreData state with setScoreData. This latter will trigger a re-render, and also scoreData has changed, so the second useEffect will run.
When the render takes place, all the code within your component will run, including the userInfo declaration (creating a new reference to it, unless localStorage.getItem("user_info") is returning undefined).
React detects userInfo as changed, so the first useEffect will run again.
The process repeats from step 3.
You could replace your
const userInfo = JSON.parse(localStorage.getItem("user_info"));
with
const userInfo = React.useRef(JSON.parse(localStorage.getItem("user_info")));
and your
useEffect(() => {
getData();
}, [userInfo, filterSemester]);
with
useEffect(() => {
getData();
}, [userInfo.current, filterSemester]);
try this
const userInfo = JSON.parse(localStorage.getItem("user_info"));
const [filterSemester, setFilterSemester] = useState(SEMESTERS[0]);
const [scoreData, setScoreData] = useState(null);
useEffect(() => {
getData();
}, [localStorage.getItem("user_info"), filterSemester]);
useEffect(() => {
console.log("scoreData: ", scoreData);
}, [scoreData]);
const getData = () => {
const params = {
student_id: userInfo?.student_info?.id,
school_year_id:
userInfo?.student_info?.class_info?.grade_info?.school_year_id,
semester: filterSemester.key,
};
getStudyInfoBySchoolYear(params).then((res) => {
if (res?.status === 200) {
setScoreData(res?.data?.data);
}
});
};

useEffect dependency with Context API. My code works fine with empty array but still gives the warning

So. I get clients from context as initialState and the code below is from my listing component (listClients.js or smth) . I update the context with the data fetched from firebase. Everything works fine actully with using empty array as dependency. I get my final array listed on my list component.. But eslint is still saying me that I should add "clientsRef" and "updateClients" into dependency but this causes an infinite loop. So what should I do with that? Close my eyes to this warning?
const { clients, removeClient, updateClients } = useContext(ClientsContext);
const collection = 'clients';
const clientsRef = firestore.collection('clients').orderBy('createdAt', 'desc');
useEffect(() => {
const unsubscribeFromSnapshot = clientsRef.onSnapshot(async snapshot => {
const clientsMap = convertSnapshotToMap(snapshot);
updateClients(clientsMap);
});
return () => {
unsubscribeFromSnapshot();
}
}, []);
You can declare the clientsRef within the useEffect and for updateCloients function you can make use of useCallback in the ContextProvider. Once you do that you can add them as a dependency to useEffect
const { clients, removeClient, updateClients } = useContext(ClientsContext);
useEffect(() => {
const collection = 'clients';
const clientsRef = firestore.collection('clients').orderBy('createdAt', 'desc');
const unsubscribeFromSnapshot = clientsRef.onSnapshot(async snapshot => {
const clientsMap = convertSnapshotToMap(snapshot);
updateClients(clientsMap);
});
return () => {
unsubscribeFromSnapshot();
}
}, []);
In ClientContext Provider
const updateClients = useCallback(() => {
// logic here
}, []);
However if you are sure that you just want the logic within useEffect to run once and not anytime later, you can disable the warning with
// eslint-disable-next-line react-hooks/exhaustive-deps
Ex:
useEffect(() => {
const unsubscribeFromSnapshot = clientsRef.onSnapshot(async snapshot => {
const clientsMap = convertSnapshotToMap(snapshot);
updateClients(clientsMap);
});
return () => {
unsubscribeFromSnapshot();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
For more details please check this post:
How to fix missing dependency warning when using useEffect React Hook?

ReactJS 16.13.1 Hook incoherent state in listener

I don't understand which magic operate here, I try everything is come up in my mind, I can't fix that problem.
I want to use an array which is in the react state of my component in a websocket listener, when the listener is triggered my state is an empty array, however I set a value in an useEffect.
Here my code :
function MyComponent() {
const [myData, setMyData] = useState([]);
const [sortedData, setSortedData] = useState([]);
useEffect(() => {
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
}, []);
useEffect(() => {
setSortedData(sortTheArray(myData));
}, [myData]);
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData]; //<=== my probleme is here, whatever I've tried myData is always an empty array, I don't understand why
/**
* onHandleEvent is an external function, just push one new object in the array no problem in the function
*/
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
};
return (
<div>
// display sortedData no problem here
</div>
);
}
Probably missed something obvious, if someone see what.
Thanks !
From the docs:
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
Here handleEvents is called from useEffect on mount and hence it sees only the initial data ([]). To catch this error better, we can move the functions inside useEffect (unless absolutely necessary outside)
useEffect(() => {
const handleEvents = (socket) => {
socket.on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
const socket = getSocket(info.socketNamespace); // wrap in socket namespace
handleEvents(socket);
});
return () => {
socket.off('EVENT_NAME', handleThisEvent);
}
}, [myData, onHandleEvent]);
Now, you can see that the useEffect has dependencies on myData and onHandleEvent. We did not introduce this dependency now, it already had these, we are just seeing them more clearly now.
Also note that we are removing the listener on change of useEffect. If onHandleEvent changes on every render, you would to wrap that with useCallback in parent component.
Is it safe to omit a function from dependencies - Docs
you need to use useMemo hook to update the function once the array value changed unless Hooks Component will always use initial state value.
change the problem part to this and try
const handleThisEvent = useMemo(payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
setMyData(myDataCloned);
},[the values you need to look for(In this case myData)]);
I finaly come up with a code like that.
useEffect(() => {
let socket;
someAxiosCallWrapped((response) => {
const infos = response.data;
setMyData(infos.data);
socket = getSocket(info.socketNamespace); // wrap in socket namespace
setSocket(socket)
});
return () => {
if(socket) {
socket.disconnect();
}
}
}, []);
useEffect(() => {
if(socket) {
const handleEvents = (socket) => {
socket.off('EVENT_NAME').on('EVENT_NAME', handleThisEvent);
};
const handleThisEvent = payload => {
const myDataCloned = [...myData];
onHandleEvent(myDataCloned, payload);
};
}
}, [socket, myData, onHandleEvent]);
Thanks to Agney !

How can I make react's useEffect to stop rerender in an infinite loop even with a dependency specified?

I am working on a small CRUD fullstack app with react and mongodb and I have this problem where I use useEffect to make an axios get request to the server to get all of my todos. The problem is that useEffect does it's job but it also rerenders to infinity. This is my component:
export default function () {
...
const [todos, setTodos] = useState([]);
const currentUser = JSON.parse(localStorage.getItem('user'))._id;
useEffect(() => {
async function populateTodos () {
try {
const res = await axios.get(`http://localhost:8000/api/all-todos/${currentUser}`);
setTodos(res.data);
} catch (err) {
if (err.response) {
console.log(err.response.data);
console.log(err.response.status);
console.log(err.response.headers);
} else if (err.request) {
console.log(err.request);
} else {
console.log('Error: ', err.message);
}
}
}
populateTodos();
}, [todos]);
console.log(todos);
return (
...
);
}
So what I was expecting to happen is that that console.log to get printed only when the todos changes, like when I add a new todo and so on, but instead it gets printed forever.
You said that you need to fetch todos at first, and whenever todos change. I can suggest you a different approach, using one more variable, something like this:
const TodosComponent = (props) => {
const [todos, setTodos] = useState([]);
const [updatedTodos, setUpdatesTodos] = useState(true);
const fetchFunction = () => {
// In here you implement your fetch, in which you call setTodos().
}
// Called on mount to fetch your todos.
useEffect(() => {
fetchFunction();
}, []);
// Used to updated todos when they have been updated.
useEffect(() => {
if (updatedTodos) {
fetchFunction();
setUpdatesTodos(false);
}
}, [updatedTodos]);
// Finally, wherever you update your todos, you also write `updateTodos(true)`.
}

React useEffect but after set state value and only once

I'm trying to migrate some of my old componentDidMount code to the new useEffect hooks and I'm having problems figuring out how to emulate the callback behavior of setState
I have an array of stuff that gets pulled from an api, I need to call a function only after the state and been loaded and then only once
Previous code:
ComponentDidMount() {
const response = await getMyArrayFromAPI
this.setState({ myArray }, () => { initializeArray() })
}
Current code:
const [myArray, setMyArray] = useState([])
useEffect(() = {
const response = await getMyArrayFromAPI
setMyArray(response)
}, [])
useEffect(() => {
// one time initialization of data
// initially gets called before myArray has value, when it should be after
// gets called every time myArray changes, instead of only once
}, [myArray])
you can set myArray in the first useEffect function, but if you want to use separate functions you can just check if it's empty
useEffect(() => {
if (!myArray.length) {
// one time initialization
}
}, [myArray])
You can use the state to drive whether or not initializeArray needs to run e.g.
const [array, setArray] = useState(null);
useEffect(() => {
getMyArrayFromAPI.then(data => setArray(data || []));
}, []);
if (array) {
// this will only ever run once as we don't set `array`
// anywhere other than `useEffect`
initializeArray();
}
Depending on what initializeArray actually does, you could run it from inside then but that's entirely up to you.
I guess you could create a custom setState hook to manage your callback
const useMyCustomStateHook = (initState, cb) => {
const [customState, updateCustomState] = useState(initState);
useEffect(() => cb(customState), [customState, cb]);
return [customState, updateCustomState];
};
So you could then have
import React, {useState,useEffect} = from 'react'
const [myArray, setMyArray] = useMyCustomStateHook([], initializeArray)
useEffect(() = {
const response = await getMyArrayFromAPI
setMyArray(response)
}, [])

Resources