I have been building a Laravel and React app and I had encountered something very embarassing.
The state variable context value is not changing with setState function. The code is following.
const ApiProvider = ({ children }) => {
const [data, setData] = React.useState({})
const [loading, setLoading] = React.useState(true)
const [repNumbers, setRepNumbers] = React.useState({})
useEffect(() => {
const fetchData = async() => {
}
fetchData()
return () => {
setData({})
}
}, [])
return <ApiContext.Provider value = {
{
repData: data,
loading,
repNumbers, //this is the state variable
setRepNumbers //this is the setState function
}
} > {
children
} <
/ApiContext.Provider>
}
In the consumming component
const { repData, repNumbers, setRepNumbers } = React.useContext(ApiContext)
const [pageLoading, setPageLoading] = React.useState(true)
useEffect(() => {
const fetchData = async () => {
setPageLoading(true)
await Axios({
})
.then((res) => {
setRepNumbers({...repNumbers, [id]: res.data })
setPageLoading(false)
return false
})
.catch((err) => {
return {}
})
return false
}
fetchData()
}, [])
If there are 2 consuming components, there should be 2 api calls and the repNumbers state should be mutated 2 times and add 2 id data but it only contains one id and if other call resolves, it replace the former id.
So how can I get both ids in repNumbers state?
This: ...but it only contains one id and if other call resolves, it replace the former id.
Assuming React 18 from your question. If so, React "Batches" updates. So although two updates were made, only the very last one was recorded.
This is dicussed in this blog post.
You can consider flushSync()
You might also consider refactoring your code to avoid the situation in the first place.
Related
I have a simple component that makes an async request when some state changes:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
fetchResult(state).then(setResult);
}, [state]);
return (
<div>{result}</div>
);
};
The problem is, sometimes the state changes twice in a short lapse of time, and the fetchResult function can take a very different amount of time to resolve according to the state value, so sometimes this happens:
As you can guess, as state now is state2 and not state1 anymore, I would like result to be result2, ignoring the response received in the then of the -obsolete- first effect call.
Is there any clean way to do so?
I would suggest you setup some kind of request cancellation method in the useEffect cleanup function.
For example with axios, it looks like that:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
const source = axios.CancelToken.source();
fetchResult({state, cancelToken: source.cancelToken }).then(setResult);
return () => {
source.cancel()
}
}, [state]);
return (
<div>{result}</div>
);
};
You have a similar API with fetch called AbortController
What this will do is it will cancel the stale requests if your state changed so only the last one will resolve (and set result).
I've not tested this... but my initial thought would be if you have the state in the response, you could check if the state fetched matches the current state. If not, then the state has changed since the request and you no longer care about the response so don't set it.
useEffect(() => {
fetchResult(state).then((response) => {
response.state === state ? setResult(response.data) : false;
});
}, [state]);
You might also be able to do it by keeping a record of the fetchedState on each request.. and again discard it if it no longer matches.
useEffect(() => {
let fetchedState = state;
fetchResult(fetchedState).then((response) => {
fetchedState === state ? setResult(response) : false;
});
}, [state]);
I've built something like the below in order to only ever use the last result of the last request sent:
const REQUEST_INTERVAL = 2000
const MyComponent = () => {
const [inputState, setInputState] = useState();
const [result, setResult = useState()
const requestIndex = useRef(0)
useEffect(() => {
const thisEffectsRequestIndex = requestIndex.current + 1
requestIndex.current = thisEffectsRequestIndex
setTimeout(() => {
if(thisEffectsRequestIndex === requestIndex.current) {
fetch('http://example.com/movies.json')
.then((response) => {
if(thisEffectsRequestIndex === requestIndex.current) {
setResult(response.json())
}
})
}
})
, REQUEST_INTERVAL)
}, [inputState])
return <div>{result}</div>
}
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 was wondering what would be the best way to get multiple data from Firebase in an async function to wait for some data from the first request. I'm using this code right now but it's not reliable and it breaks sometimes saying that it can't fetch the data for the second call as it's undefined.
function useOccasion() {
const [occasionData, setOccasionData] = useState(null)
const [friend, setFriend] = useState(null)
let { occasion } = useParams()
useEffect(() => {
const unsubscribe = firestore.collection('occasions').doc(occasion)
.onSnapshot(async eventData => {
setOccasionData({id: eventData.id, ...eventData.data()})
let friendData = await firestore.collection("friends").doc(eventData.data().friend).get();
setFriend({id: friendData.id, ...friendData.data()});
})
return () => unsubscribe()
}, [occasion])
return [occasionData, friend]
}
If there's a more robust way to achieve this that would be amazing.
I'd suggest breaking your data fetches into two hooks, one for each collection occasion and friends.
I'm not sure how you've set firebase up, but I access it through context.
e.g.
const useOccasion = () => {
const firebase = useContext(FirebaseContext)
const [occasions, setOccasions] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = firebase.db.collection('occasions')
.onSnapshot(snapshot => {
if (snapshot.size) {
let occasionList = []
snapshot.forEach(doc =>
occasionList.push({ ...doc.data(), uid: doc.id }),
)
setOccasions(occasionList)
setLoading(false)
} else {
setOccasions([])
setLoading(false)
}
})
return () => {
unsubscribe()
}
}, [])
return { occasions, loading }
}
Finally, in your component where you require the data you can access this hook:
const { occasions, loading } = useOccasion()
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)`.
}
I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}