Why is local storage deleted when I refresh the page? - javascript

I have a reducer for state management at the context API. I was saving my Todos and it's happening successfully but when ı refresh the page all todos is deleting and just stay empty array.
// The relevant part in the reducer.
case "TODO_ADD_USER":
return {
...state,
users: action.payload,
};
// Localstorage functions.
useEffect(() => {
saveLocalTodos();
}, [state]);
useEffect(() => {
const localTodos = getLocalTodos();
dispatch({ type: "TODO_ADD_USER", payload: localTodos });
}, []);
const saveLocalTodos = () => {
if (localStorage.getItem("users") === null) {
localStorage.setItem("users", JSON.stringify([]));
} else {
localStorage.setItem("users", JSON.stringify(state.users));
}
};
const getLocalTodos = () => {
let locals;
if (localStorage.getItem("users") === null) {
locals = [];
} else {
locals = JSON.parse(localStorage.getItem("users"));
}
return locals;
};
Place of keeping the state.
const users = {
users: [],
};

There are a couple issues with your code.
The biggest one here is that you are saving the todos before getting them. So at the start of the application, things are getting reset, which is problematic.
Up next, you have your condition for the saving a bit backwards. You want to check if state.users === null and do a special action for that, rather than if localStorage.getItem("users") === null, as that will be null by default, and have nothing to do with the value in memory.
In fact, if the localStorage value is not null, but the state.users is, then it would set "null" to localStorage, which is less than ideal.
Here's the working code:
useEffect(() => {
// Get the item from local storage. JSON.parse(null) returns null rather than throws
// Get from local storage before setting it
const localTodos = JSON.parse(localStorage.getItem("users")) || [];
dispatch({ type: "TODO_ADD_USER", payload: localTodos });
}, []);
useEffect(() => {
// The conditions for this was wrong.
// You want to check if `state.users` has a value
// If it does, store! If not, don't store.
// That way you don't reset data
// In the case that you have this app running in two windows,
// there's more that needs to be done for that.
if (state.users) {
localStorage.setItem("users", JSON.stringify(state.users || []));
}
}, [state]);
https://codesandbox.io/s/angry-glitter-9l10t?file=/src/App.js

Related

localStorage is not defined in Nextjs, Redux and Typescript

I'm facing a problem with my project. The problem is ReferenceError: localStorage is not defined. I'm using Nextjs, and Redux with Typescript.
const storedUser: string | null = localStorage.getItem('user');
const user: DisplayUser | null = !!storedUser ? JSON.parse(storedUser) : null;
const storedJwt: string | null = localStorage.getItem('jwt');
const jwt: Jwt = !!storedJwt ? JSON.parse(storedJwt) : null;
I'm using these 2 variables user and jwt in here initialState
const initialState: AuthState = {
user: user,
jwt: jwt,
isLoading: false,
isSuccess: false,
isError: false,
}
And initialState are used in authSlice function
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
reset: (state) => {
state.isLoading = false;
state.isSuccess = false;
state.isError = false;
}
},
})
If you are using React.js or Next.js and you want to check :
if you are on the Browser (which means you can use the localStorage)
or if you are on the Server (which means you cannot use localStorage)
You need to check if the window variable is not undefined, example :
if (typeof window !== 'undefined') {
console.log('You are on the browser')
// 👉️ can use localStorage here
} else {
console.log('You are on the server')
// 👉️ can't use localStorage
}
similar discussion on github/vercel/next.js/discussions
I have a similar issue like this in which I have to get the cart items from the localStorage and pass them to the initial state of redux. I solve it like this below code.
const getFromLocalStorage = (key: string) => {
if (!key || typeof window === 'undefined') {
return ""
}
return localStorage.getItem(key)
}
And then use it in initial state like below code:
export const initialState = { cartItems: getFromLocalStorage("cartItems") ? JSON.parse(getFromLocalStorage("cartItems") || '{}') : []};
Adding onto Omar's answer - if you need to store something from localStorage in a Redux store, you'd need to grab that information client-side and then pass it into your initial Redux store. Redux itself will not be able to differentiate between client/server side rendering unless you check typeof window !== 'undefined'.
If you want to use Omar's answer as-is, you would need to make that calculation inside your Redux reducer when the initial state is being calculated. I don't recommend bringing that logic inside your reducer (makes it harder to unit test the reducer) - try calculating that logic outside of the initial Redux store, then pass it in (minimal example below)
// However you calculate your initial Redux state should replace INITIAL_REDUX_STATE
const [statefulStore, setStatefulStore] = useState(INITIAL_REDUX_STATE);
const reduxStore = createStore(yourReducer, initialStore);
useEffect(() => {
// Once and only once on the initial render, grab the localStorage value
// and update Redux state
setStatefulStore(oldState => {
return Object.assign(oldState, {
ssrValue: localStorage.getItem('user')
});
});
}, []);
You cannot run localStorage in Node because it's a browser feature. You can run a util and reuse it like this:
const getFromLocalStorage = (key) => {
if (!key || typeof window === 'undefined') {
return ""
}
return localStorage.getItem('user')
}
You can import this in other files and use it:
const userId = getFromLocalStorage('userId');
This is the simplest way of using this.

Creating UseEffect react hook which should go through JSON data and switch the state

I'm trying to build filtering for job locations ("remote"/"in-person"/"hybrid") for my personal project and I was trying to troubleshoot it for quite some time (very new to programming). I am sure I made mistakes in my main fetchLocationData function and passing URLSearchParams but I am not gonna be surprised if there are more mistakes.....
const fetchLocationData = async () => {
const data = await getJobs();
return data.json();
};
useEffect(() => {
let condition = fetchLocationData();
switch (jobCondition) {
case 'On-site': condition.filter((con) => con.jobLocation === 'in_person');
break;
case 'Remote': condition.filter((con) => con.jobLocation === 'remote');
break;
case 'Hybrid': condition.filter((con) => con.jobLocation === 'hybrid');
break;
default:
condition = null;
}
if (condition != null) {
const params = new URLSearchParams({
jobs: jobFilter || null,
location: locationFilter || null,
since: datePosted || null,
conditions: `${condition.con}`,
});
history.push({ pathname: '/', search: `${params.toString()}` });
return;
}
const params = new URLSearchParams({
jobs: jobFilter || null,
location: locationFilter || null,
since: null,
conditions: null,
});
history.push({ pathname: '/', search: `${params.toString()}` });
}, [jobCondition]);
Rather than creating switch statement you can somewhat optimize your code this way.
let condition = fetchLocationData();
let obj={'On-site':'in_person','Remote':'remote','Hybrid':'hybrid'}
condition =condition.filter((con) => con.jobLocation === obj[jobCondition]);
There's a few issues with your code. I rewrote it and I think this is what you're looking for.
I did not know what kind of data getJobs() returned but I'm presuming it's an array of objects because you are filtering through it while trying return object data.
First thing first, move stuff out of useEffect. Create it's own function to handle all of functionality. Let's call it jobFilterHandler.
This jobFilterHandler will be wrapped in a useCallback. This is being done because while the function itself will be called in a useEffect, the data is being fetched and processed in this function. So we want this to return a memoized result to prevent unnecessary data fetching and re-renders.
I will add comments in the code to explain what it does.
const jobFilterHandler = useCallback(async (jobCondition) => {
// This function takes your filter_condition as an argument and is
// asynchronous as marked above.
const jobs = await getJobs(); // We retrieve the list of jobs from the API and wait for it.
// Here we define the possible filter options to match with the raw data.
const condition_options = {
"On-site": "in_person",
Remote: "remote",
Hybrid: "hybrid"
};
// From the data we received, we filter the jobs that match our filter.
const filtered_jobs = await jobs.filter(
(job) => job.jobFilter === condition_options[jobCondition]
);
// If there are no filtered jobs, the function ends.
if (!filtered_jobs) {
return;
}
// Else the filtered jobs will be returned.
return filtered_jobs;
}, []);
useEffect(() => {
// << here you need to write your code to fetch your jobCondition value >>
jobFilterHandler(jobCondition).then((jobs) => {
// << do whatever you want with the filtered jobs data here >>
console.log(jobs);
});
}, [jobCondition, jobFilterHandler]);
Uncommented code here.
const jobFilterHandler = useCallback(async (filter_condition) => {
const jobs = await getJobs();
const condition_options = {
"On-site": "in_person",
Remote: "remote",
Hybrid: "hybrid"
};
const filtered_jobs = await jobs.filter(
(job) => job.jobFilter === condition_options[filter_condition]
);
if (!filtered_jobs) {
return;
}
return filtered_jobs;
}, []);
useEffect(() => {
jobFilterHandler(jobCondition).then((jobs) => {
console.log(jobs);
});
}, [jobCondition, jobFilterHandler]);

Loading state not effective in the React application (MobX)

I am coding a React app that fetches data from WordPress REST API. Everything works fine so far, however, loading indicator does not show up. I use MobX for state management. I have a loadingInitial observable. Whenever I start my action, I set this observable true to get into loading state. After the action does necessary operations, I reset loadingInitial to false. So I expect to see loading screen while fetching posts. But I see blank page while the daha is loading.
Here are the code for the action:
#action loadAnecdotes = async (page: number, year: number, order: string) => {
this.loadingInitial = true
try {
const anecdotesHeaders = year === 0 ? await agent.AnecdotesHeaders.list() : await agent.AnecdotesHeaders.listByYear(year)
const maxPages = anecdotesHeaders['x-wp-totalpages']
if (page <= maxPages) {
const anecdotes = year === 0 ? await agent.Anecdotes.list(page, order) : await agent.Anecdotes.listByYear(page, year, order)
runInAction(() => {
this.anecdoteArray = []
anecdotes.forEach((anecdote, i) => {
this.anecdoteArray[i] = anecdote
})
this.loadingInitial = false
})
} else {
runInAction(() => {
this.loadingInitial = false
})
}
return {anecdoteArray: this.anecdoteArray, maxPages}
} catch (error) {
console.log(error)
this.loadingInitial = false
}
}
Here is my useEffect in the component where I fetch the posts:
useEffect(() => {
loadAnecdotes(page, year, order).then(result => {
if (page <= result?.maxPages)
setArray(a => [...a, ...result?.anecdoteArray!])
setLoaded(true)
})
}, [loadAnecdotes, page, year, order, setLoaded, setArray])
Here is the what I call just before the returning the posts:
if (loadingInitial) return <LoadingComponent content='Anecdotes loading...' />
As a side note, my component is set as an observer.
What could I be doing wrong?
Well, it seems loadingInitial is not enough alone. I added a second condition to it, the length of the posts array. If it is 0, show the loading component. And it worked! Just this edit solves the problem:
if (loadingInitial || !array.length) return <LoadingComponent content='Anecdotes loading...' />

Getting items from localstorage returns null

I have a React app that loads items from a api call from a database, if there is no data in local storage. I will then set the state with data and load the data in local storage. Next time the app loads it will take the data from local storage instead. The data is mapped out in the return statement.
The Problem is it still returns null even when there is data in local storage.
I will load the data from componentDidMount:
The below code will run the function "loadItems" that first check if there is any data in localstorage (name "workItems") and if so, store it in the state. If it isn't, it will call the database in an api call, store the data to state and also store the data in localstorage which will be used next time the component mounts. I have confirmed that the data is stored in the browser. But when the data from the localstorage exist, is mapped to the state and finally mapped out in the return from the render function it will complain the data is "null". How come? The data is stored in the local storage and exist there when I inspect it from the dev tools.
componentDidMount() {
this.loadItems(false);
}
async loadItems(forcedUpdate) {
const payload = {
forcedUpdate: forcedUpdate
};
if (localStorage.getItem('workItems').length > 0) {
let data = localStorage.getItem("workItems");
this.setState({
workitems: localStorage.getItem("workItems"),
loading: false,
sources: allSources,
showSources: allSources,
}, () => {
return;
});
}
var apiUrl = "api/WorkItem/GetWorkItems";
const response = await Axios.default.post(apiUrl, payload);
console.log(response);
var allSources = response.data.items
.map(item => item.source)
.filter((value, index, self) => self.indexOf(value) === index);
this.setState({
workitems: response.data.items,
loading: false,
sources: allSources,
showSources: allSources,
failedScrape: response.data.failedscrape,
lastUpdated: response.data.lastUpdated
});
localStorage.setItem('workItems', response.data.items);
}
localStorage.setItem(key, value) expecting a value to be string. When you pass the object such as [{id: 1}], it will typecast it to string. Hence the object becomes the string like this "[object Object]".
localStorage.setItem('test', [{id: 1}]);
const item = localStorage.getItem('test');
console.log(item) // "[object Object]" a string
console.log(item[0]) // "["
console.log(item[1)) // "o"
Solution
The solution is to stringify it before saving to localStrage and parse it after getting the item.
localStorage.setItem('workItems', JSON.stringify(response.data.items));
// when you get the items
if (JSON.parse(localStorage.getItem('workItems')).length > 0)
You can use this hook. Otherwise, to fetch data from local storage, use these functions:
const getValue = (key, defaultValue = {}) => {
try {
// read value from local storage
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.log(error);
return defaultValue;
}
}
const setValue = (key, value) => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.log(error);
}
}
setValue("test", {test: "Test value"})
console.log(getValue("test"))

Vue, Vuex, JavaScript: includes() does not work as expected

I wanna store some objects inside an array if the array doesn't already contain some object with the same id. anyways, everything works fine til i start adding more than one object at a time.
Here is the related code using Vuex:
// filter function to check if element is already included
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
return sessionItem._id.includes(val._id);
}
};
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
// Pre filtering exercises and prevent duplicated content
if( checkForDuplicate(payload) === true ) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
As I wrote before everything works fine as long i ad a single object, checkForDuplicate() will find duplicated objects and deny a push to the array.
now there is a case in which I wanna push a bundle of objects to the array, which i am doing through an database request, looping through the output, extracting the objects and pushing them through the same function as I do with the single objects:
// get user related exercises from database + clear vuex storage + push db-data into vuex storage
addSessionWorkout: ({ commit, dispatch }, payload) => {
axios.post(payload.apiURL + "/exercises/workout", payload.data, { headers: { Authorization: "Bearer " + payload.token } })
.then((result) => {
// loop through output array and
for( let exercise of result.data.exercises ) {
// push (unshift) new exercise creation to userExercises array of vuex storage
dispatch("storeSessionExercises", exercise)
};
})
.catch((error) => {
console.error(error)
});
},
The push does also work as it should, the "filter function" on the other hand doesn't do its job. It will filter the first object and deny to push it to the array, but if there is a second one that one will be pushed to the array even inf the same object (same Id) is already included, what am I not seeing here!? makes me nuts! :D
I understand it like the loop will put each object through the checkForDuplicate() and look if there is an duplicate it should output true, so the object doesn't get pushed into the array. If anybody sees what I currently don't just let me know.
the mistake is your filter function. you want to loop over your sessionExercises and only return true if any of them matches. However, at the moment you return the result of the very first check. Your loop will always only run one single time.
Option 1: only return if matched
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
if (sessionItem._id.includes(val._id)) {
return true;
}
}
return false;
};
Option 2: use es6 filter
storeSessionExercises: (state, payload) => {
var exercises = state.sessionExercises.filter(ex => (ex._id.includes(payload._id)));
if(exercises.length) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
}
I would change the addSessionWorkout action, I would create a new exercises array with the old and new entries and then update the state.
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
state.sessionExercises = payload;
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
addSessionWorkout: async({
commit,
dispatch,
state
}, payload) => {
const result = await axios.post(payload.apiURL + "/exercises/workout", payload.data, {
headers: {
Authorization: "Bearer " + payload.token
}
})
try {
const newExercices = result.data.exercises.reduce((acc, nextItem) => {
const foundExcercise = acc.find(session => session.id === nextItem.id)
if (!foundExcercise) {
return [...acc, nextItem]
}
return acc
}, state.sessionExercises)
dispatch("storeSessionExercises", foundExcercise)
} catch (e) {
console.error(error)
}
},

Categories

Resources