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)
}, [])
Related
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
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);
}
});
};
I have the following code and I wonder how I can improve performance, specifically, should I move the const fuse = new Fuse... section and the buildSearchRequest function within useEffect so it is called only when the search query is changed? I have noticed my code that consumes the custom hooks hits the new Fuse section many times.
const [searchResults, setSearchResults] = React.useState([])
const fuse = new Fuse(DummySearchResponse.results, {
keys: ["data.programmeTitle"],
includeScore: true,
threshold: 0.2,
})
const searchApiUrlStart = "http://mimir.prd.oasvc.itv.com/search?query="
const searchApiUrlEnd =
"&entityType=programme&streamingPlatform=itv_hub&checkAvailability=true"
const buildSearchRequest = (searchString) => {
return (
searchApiUrlStart +
encodeURIComponent(searchString) +
searchApiUrlEnd
)
}
React.useEffect(() => {
if (!query) return
const fetchData = async () => {
let searchData
if (useLiveSearchApi) {
const liveResponse = await fetch(
"http://mimir.prd.oasvc.itv.com/search?query=" +
buildSearchRequest(query) +
"&entityType=programme&streamingPlatform=itv_hub&checkAvailability=true"
)
const liveJson = await liveResponse.json()
const liveResults = await liveJson.results
searchData = liveResults
} else {
const fuseResponse = await fuse.search(query)
const fuseJson = await fuseResponse.map((result) => {
return result.item
})
searchData = fuseJson
}
const mappedResults = await searchData.map((searchItem) => ({
title: searchItem.data.programmeTitle,
contentImageUrl: searchItem.data.imageHref,
programmeCCId: searchItem.data.programmeCCId,
episodeId: searchItem.data.episodeId,
}))
setSearchResults(mappedResults)
}
fetchData()
}, [query])
return { searchResults }
}```
First and foremost, avoid declaring a callback within the useEffect. What you need to do is use the useCallBack hook to declare your fetchData callBack
Your code should atleast look like...
const fetchData = useCallBack(() => {
// Your Fetch Data Code
}, [<dependency array>]) // Here, you want to add all dependencies whose current state you need. Note that if a dependency is not added here, and you use it within the useCallBack, you'll only access a stale state (state during initialization), and never the updated dependency state.
// Here are three ways you can declare your useEffect
useEffect(fetchData, [query]); You probably wanna use this one. Less lines and much cleaner.
useEffect(() => fetchData(), [query]);
useEffect(() => {
fetchData();
}, [query]);
Your useEffect will only be called once the query variable has been updated. So, to ensure a sideEffect is not triggered, ensure your useEffect or the useCallBack specified as the trigger does not update the variable. If this is the case, your code will be stuck in an indefinite loop.
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 !
I've got a question about useEffect and useState inside of it.
I am building a component:
const [id, setId] = useState(0);
const [currencies, setCurrencies] = useState([]);
...
useEffect(()=> {
const getCurrentCurrency = async () => {
const response = await fetch(`https://api.exchangeratesapi.io/latest?base=GBP`);
const data = await response.json();
const currencyArray = [];
const {EUR:euro ,CHF:franc, USD: dolar} = data.rates;
currencyArray.push(euro, dolar/franc,1/dolar);
console.log("currencyArray", currencyArray);
setCurrencies(currencies => [...currencies, currencyArray]);
}
getCurrentCurrency();
}, [id, currencies.length]);
Which is used for making a new API request when only id change. I need to every time that ID change make a new request with new data. In my case now I have infinite loop. I try to use dependencies but it doesn't work as I expected.
You changing a value (currencies.length), which the useEffect depends on ([id, currencies.length]), on every call.
Therefore you cause an infinite loop.
useEffect(() => {
const getCurrentCurrency = async () => {
// ...
currencyArray.push(euro, dolar / franc, 1 / dolar);
// v The length is changed on every call
setCurrencies(currencies => [...currencies, currencyArray]);
};
getCurrentCurrency();
// v Will called on every length change
}, [id,currencies.length]);
You don't need currencies.length as a dependency when you using a functional useState, currencies => [...currencies, currencyArray]
useEffect(() => {
const getCurrentCurrency = async () => {
...
}
getCurrentCurrency();
}, [id]);
Moreover, as it seems an exchange application, you might one to use an interval for fetching the currency:
useEffect(() => {
const interval = setInterval(getCurrency, 5000);
return () => clearInterval(interval);
}, []);
you can just call the useEffect cb one the component mounted:
useEffect(()=>{
//your code
// no need for checking for the updates it they are inside the component
}, []);