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");
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 can load my data but only after I refresh the page. Until then, it shows the data from the previous item I clicked on. It's behaving like a cache would.
Here is my mediaSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const KEY = process.env.REACT_APP_API_KEY
const BASE_URL = process.env.REACT_APP_BASE_URL
const HBO_SINGLE_MEDIA_API = `${BASE_URL}/titlestest`
const initialState = {
media:{},
status: 'idle', //'idle', 'loading', 'succeeded', 'failed'
error:null
}
export const fetchSingleMediaTitle = createAsyncThunk(
'media/fetchSingleMediaTitle',
async (id) => {
const response = await axios.get(
HBO_SINGLE_MEDIA_API,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': KEY,
},
params: {
titleId: id,
}
}
)
return response.data.Item;
}
)
const mediaSlice = createSlice({
name: 'media',
initialState,
reducers:{},
extraReducers: {
[fetchSingleMediaTitle.pending]: () => {
console.log("Pending");
},
[fetchSingleMediaTitle.fulfilled]: (state, { payload }) => {
state.status = 'succeeded'
state.media = payload
},
[fetchSingleMediaTitle.rejected]: () => {
console.log("Rejected");
},
}
})
// SELECTORS
export const selectSingleMedia = (state) => state.media.media;
export const getMediaStatus = (state) => state.media.status;
export default mediaSlice.reducer
And then the Media Component has what you would expect
const [media, setMedia] = useState([]);
const {id} = useParams();
const dispatch = useDispatch();
const singlemedia = useSelector((state) => selectSingleMedia(state, id))
const mediaStatus = useSelector(getMediaStatus)
useEffect(() => {
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id)) //yes, it's imported
}
setMedia(singlemedia);
//it returns the array with the data but not the current one
console.log("singlemedia: ", singlemedia);
return () => { };
// Someone suggested removing the dependencies below but then it doesn't load anything.
}, [id, singlemedia, mediaStatus, dispatch, media_data])
I am at a loss here because as I understand it, the useEffect is supposed to fire onload and give me the current data. The ID is correct in the params but the state is not mutating.
Thanks in advance
EDIT
For reference, here is the console log. Those console logs are in the useEffect. The API is slightly slower than the useEffect and the render is happening before the data is ready (race condition) or at least that's what I think it's happening here that's why it loads empty and then it loads again. But the confusing part is that ALL of that happens on a page refresh only. On a normal load the state is not empty is loaded and in time for the UI to render it (no race condition), only, it's loaded with the old data from a previous state shape
Here is the redux dev tools
Your selector takes one argument, but you're passing it two. Change it to:
const singlemedia = useSelector(selectSingleMedia);
Second, your singlemedia is the state. There's no need to setMedia(singlemedia);.
Your useEffect should look like:
useEffect(() => {
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id));
}
}, [dispatch, id, mediaStatus]);
Also, you should look into RTK Query, which would replace createAsyncThunk: https://redux-toolkit.js.org/tutorials/rtk-query
Edit per our discussion:
useEffect(() => {
dispatch(fetchSingleMediaTitle(id));
}, [dispatch, id]);
The problem is very simple, just remove the media internal state variable:
const [media, setMedia] = useState([]);
As what you are doing now is that you send the async request here:
if (mediaStatus === 'idle') {
dispatch(fetchSingleMediaTitle(id)) //yes, it's imported
}
And before the backend responds you read the store and set it in the internal state:
setMedia(singlemedia); // This is old data now, as we are waiting for the backend
If you wish to display the store state just use singlemedia in the render method.
If you wish to have some temporary state that mimics a backend response again use singlemedia and implement the temporary state in the redux store.
PS. the useEffect should depend only on id and useDispatch
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.
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]);
React code for storing Data from API to an Array and Using the same Array's event_date value for further use.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
let today = new Date();
console.log(holidayPlans);
holidayPlans.filter((date) => {
const eventDate = new Date(date.event_date);
console.log(eventDate);
});
};
So what the thing is when i use the Same (holidayPlans) array to display some contents in html it shows the values and displays properly but when i use inside a function it shows there is no data inside the array .
console.log(holidayPlans) shows this
Same Array used to display in html
Here's a challenge: write a JavaScript function useState such that the console.log outputs a 4 and then a 5:
function render() {
let [thing, setThing] = useState(4);
console.log(thing); // 4
setThing(5);
console.log(thing); // 5
}
No matter what you do, you'll never be able to write this function, because no external JavaScript function will be able to set the value of the thing variable; that's because an external JavaScript has no way to modify the thing variable. All useState would be able to do is set its own internal state and change what it returns. Silly example here:
let hiddenState;
function useState(initialValue) {
if (hiddenState === undefined) {
hiddenState = initialValue;
}
const setState = value => {
hiddenState = value;
}
return [hiddenState, setState];
}
That means render will only be able to get a new value if useState is called again:
function render() {
let [thing, setThing] = useState(4);
console.log(thing); // 4
setThing(5);
[thing, setThing] = useState(4);
console.log(thing); // 5
}
This is essentially what useState does but in a way where the hidden state is unique per instance. As you can see, setState is to be considered "asynchronous" in that state changes aren't reflected until the next render. setState queues up a re-render request. The next time your render function is called, useState will be called again, and it will return a new value.
Notice with these code modifications, rather than us referencing the state variable before it has updated, we can still reference your response object to get the data:
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
// On the first rendering of `UpcomingHolidays`, holidayPlans will be [].
// After setHolidayPlans is called, a re-render will be queued, and this
// UpcomingHolidays function will be called again. When useState is called
// the second time, it will have the value passed into setHolidayPlans.
const [holidayPlans, setHolidayPlans] = useState([]);
// Same for dateArray.
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
async function getHolidayPlans() {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (!holidayResp) {
return;
}
// These will flag the component as needing to re-render after the effect
// completes. They do not change the local variables; they update the
// internal data of the useState hooks so that the next time those useState
// calls occur, they'll return new values.
setCities(holidayResp.cityModule);
setHolidayPlans(holidayResp.holidayModule);
setDate(holidayResp.holidayModule.map(date => new Date(date.event_date));
// If you want to log here, don't reference state, which hasn't updated yet.
// Either store response data as variables or reference the response itself.
console.log('Holidays are', holidayResp.holidayModule);
}
return <div>Your content</div>;
}
If you move your console.log(holidayPlans); out of getHolidayPlans function, you get an updated value.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(holidayResp.cityModule);
setHolidayPlans(holidayResp.holidayModule); // you may filter data here
setDate(holidayResp.holidayModule);
}
};
getHolidayPlans();
}, []);
console.log(holidayPlans);
This happens because when you use the useState hook, you are assigning the state values holidayPlans and dateArray to local constants (or variables, this does not matter), and these values are assigned each time the component is rendered. This means that the constant value in your component will not get updated immediately, but it will be reflected in the next render, which will be triggered by the state updates that you do within getHolidayPlans. This is why, if you place the console.log() call outside getHolidayPlans, the value is printed properly.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
// ...
};
console.log(holidayPlans);
Basically this is what happens:
First render
|
V
useEffect executes getHolidayPlans()
|
V
getHolidayPlans() performs state changes,
triggering a new render cycle
|
V
Second render,
which will have new state values
It is important to notice that in the end UpcomingHolidays is just a function, and its body is executed on each render cycle.
Based on this, the recommended way to go is to use constant/variables local to the caller function (getHolidayPlans()) instead of using the state constant/variables immediately after their respective setState function has been called, because they are updated after the completion of the function that it was called in.
export const UpcomingHolidays = (props: UpcomingHolidaysProps) => {
const [holidayPlans, setHolidayPlans] = useState([]);
const [dateArray, setDate] = useState([]);
useEffect(() => {
getHolidayPlans();
}, []);
const getHolidayPlans = async () => {
const holidayResp = await PortalHolidayService.getInstance().getHolidayPlans();
const holidayPlansLocal = holidayResp.holidayModule;
if (holidayResp) {
setCities(() => holidayResp.cityModule);
setHolidayPlans(() => holidayResp.holidayModule);
setDate(() => holidayResp.holidayModule);
}
let today = new Date();
console.log(holidayPlansLocal);
holidayPlansLocal.filter((date) => {
const eventDate = new Date(date.event_date);
console.log(eventDate);
});
};