useSWR() and mutate() do not behave as expected when component is unmounted - javascript

I am having an issue with some unexpected behavior with regards to mutate(key). In my data fetching code, I have these hooks/functions:
const recentOptions = {
refreshInterval: 0,
revalidateOnFocus: false,
dedupingInterval: 180000000 //or 500
}
export const useRecent = () => {
const {data, error} = useSWR(
`${config.apiUrl}/GetRecentFoundations`,
getThenResolve,
recentOptions
);
return {
recent: data,
isLoading: !error && !data,
isError: error
};
}
export const setRecent = async(fid) => {
await postThenResolve(
`${config.apiUrl}/SetFoundationRecent`,
{fid});
//TODO: So the behavior I am seeing tends to indicate that
//mutate(key) sends out a message to swr to refetch, but doesn't
//necessarily cause the cache to be invalidated.
//This means that mutate will cause any CURRENTLY MOUNTED useSWR()
//to refetch and re-render, but ones that aren't mounted will
//return with stale data.
mutate(`${config.apiUrl}/GetRecentFoundations`);
}
I also have a component that fetches data with useRecent():
const FoundationRecents = props => {
const {recent, isLoading} = useRecent();
if(isLoading) return <div>...LOADING...</div>;
if(!recent) return null;
return <SimpleCard
titlebg={colors.blue}
titlefg={colors.white}
titlesz='small'
title='Recent'
contentbg={colors.white}
component={<FoundationRecentsView recent={recent}/>}
/>
}
I use this pattern in two places currently, one where the component is mounted when the setRecent() occurs. The other, where the component is unmounted. When the component is mounted, everything works fine. When mutate() is called, everything seems to refetch and rerender.
However, if setRecent() is called when the component is unmounted, and I later return to it, I get stale data, and no refetch.
I think I must be misunderstanding what mutate(key) does, or maybe I am unclear as to what the dedupingInterval is. I thought, that even with a high deduping interval, the mutate would cause the GetRecentFoundations cache to be invalid, and thus the next call to useSWR() would require a revalidate.

Related

Set State Failing After Second Render of Function Component

I have what seems to be a really odd issue with a react function component.
For brevity my code look like:
const Clients = () => {
const [state, setState] = useState({
loading: true,
entities: [],
editing: null,
});
const ajax = useAjax(api.scopes);
const errorHandler = useErrorHandler();
useEffect(() => {
ajax.get(`${api.endpoint}/clients`)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new HttpError(response);
})
.then((json) => setState({
...state,
entities: json,
loading: false,
}))
.catch(errorHandler);
}, []);
const handleSelect = (entity) => {
console.log({
entity,
});
setState({
...state,
editing: entity,
});
};
console.log('hello world!');
const { loading, entities, editing } = state;
return (
loading ? <LinearProgress /> : (
<>
<TableComponent entities={entities} onSelect={handleSelect} />
<EditingComponent
entity={editing}
open={!!editing}
onClose={() => setState({ ...state, editing: null })}
/>
</>
)
);
};
export default Clients;
The <TableComponent /> always works as expected and the event handler is always called with the correct entity passed.
What is not happening is that the editing component only opens the first time the component is mounted (case 1). At this stage the editing component opens with the correct info, and continues to do so whichever row is clicked.
After this, if I navigate away from this component (React Router 6.4.2) and then come back to it (case 2). The entities still load, but the handleSelect function fails to update the state and nothing happens when a row is clicked (it is called however).
The useAjax hook is to support azure msal and has been working perfectly since written and the network trace doesn't show any issue accessing the API.
React is version 18.2.0 if that is relevant.
The useEffect function is called in all cases and the entities are set correctly.
The console.log in the handleSelect always shows the correct entity as selected in the table.
The second console.log ('hello world!') gets called when the page loads (in both cases) then only in the first case when a row is selected.
Using React Dev Tools it shows that the state is simply not updated in the second case.
What is really confusing me is why there is any difference between case 1 and case 2, as it doesn't matter if I navigate to a page where the component tree is doesn't include the above and I assume m references to the component are dropped. I suspect this is a misunderstanding that I have about how React and/or React Router are working.
Trying to Google the issue mainly seems to return issues about the state not updating immediately after it is set - which I am pretty sure is not the case.
It's a long explanation but you should use setState like this (in your case):
setState(previousState => ({
...previousState ,
editing: entity,
}));

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

How do I set initial state to data fetched with useQuery?

I'm working with React and Apollo and I'm trying to initialize state with data fetched using useQuery hook but I'm getting "Cannot read property 'map' of undefined" when loading the page.
const { loading, error, data } = useQuery(FETCH_BLOGS);
const [state, setState] = useState(undefined);
useEffect(() => {
if (loading === false && data) {
setState(data.blogs);
}
}, [loading, data]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
In the JSX I'm calling {renderBlogs(state)} which maps over the array and is where the error is being thrown. If I pass in the initial data {renderBlogs(data.blogs)} it works but I need to store the data in state as it will be mutated.
When I console.log(state) it logs 2 lines:
The first is undefined.
The second is the array of blogs (as shown in the screenshot).
It appears that the page is trying to render the initial state (undefined) before before the state is set to the query data. Is this the case? I thought using useEffect would solve this but that doesn't seem to be the case. Any help is appreciated, thank you.
To quote the useEffect documentation:
The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
The problem is that useEffect will trigger after a render. This will result in the following chain of events. Assume loading is just set to false and there where no errors fetching the data.
When the component is now being rendered both the if (loading) and if (error) guard clauses will be skipped, because the data successfully loaded. However state is not yet updated because the useEffect callback will trigger after the current render. At this point when you call renderBlogs(state) state will still be undefined. This will trow the error you describe.
I might be missing some context, but from what is shown in the question there is no reason to use useEffect. Instead use data directly.
The example provided by Apollo for useQuery provides a good starting point:
import { gql, useQuery } from '#apollo/client';
const GET_GREETING = gql`
query GetGreeting($language: String!) {
greeting(language: $language) {
message
}
}
`;
function Hello() {
const { loading, error, data } = useQuery(GET_GREETING, {
variables: { language: 'english' },
});
if (loading) return <p>Loading ...</p>;
return <h1>Hello {data.greeting.message}!</h1>;
}
Applying the example to your scenario it might look very similar.
const { loading, error, data } = useQuery(FETCH_BLOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return renderBlogs(data.blogs);
If you need the setter you might want to provide a bit more context to the question. However a common reason to have a setter might be if you want to apply some sort of filter. A simple solution would be to have a computed blogs state.
const filteredBlogs = data.blogs.filter(blog => blog.title.includes(search));
Here search would be the state of some <input value={search} onChange={handleSearchChange} /> element. To improve performance you could also opt to use useMemo here.
const filteredBlogs = useMemo(() => (
data.blogs.filter(blog => blog.title.includes(search))
), [data.blogs, search]);
If you really need to use useState for some reason, you could try the onCompleted option of useQuery instead of useEffect.
const [blogs, setBlogs] = useState();
const { loading, error } = useQuery(FETCH_BLOGS, {
onCompleted: data => setBlogs(data.blogs),
});

setState randomly failed which is caused by component unmount

I'm facing a weird situation ...
The setState sometimes worked fine, but sometimes will get the warning for setting state to the unmounted component.
If the user's cart is undefined, then call the API to get the cart information and set the state.
const loginData = () => {
const isUnmounted = useRef(false);
const handleData = () => {
if (isUnmounted.current) return false;
const setData = (user) => {
//setCart failed sometimes, because of setting State to the unmount component
if (cart == undefined) getUserData((response) => setCart(response.cart));
}
if (userData !== null) {
setUserData(user);
}
}
useEffect(() => {
handleData();
return () => {
isUnmounted.current = true;
}
}, [])
}
When I enter the page contain this component, it will sometimes success to work.
And the other time the page crashed because cart can't be set with the unmounted component (will get the warning in console), and the further cart.id will crash the process.
Looks like it just happened randomly...
Sometimes the process crash when I just enter the page,
but sometimes I can enter the page and refresh the page multiple times then the page crashed ...
Have no idea what is happened. May anyone give me some suggestions? Thanks!

In React, failing to stop Axios request when component unmounts

With data fetching in React, the following is a common warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in ParentComponent
I've read multiple posts and suggestions on how to handle this, and none are working currently.
For this, we have the function useAxiosApi which fetches data asynchronously, and ParentComponent which is the component that uses the useAxiosApi() and needs the data. ParentComponent is the component being unmounted / being referenced in the warnings.
Parent Component
import useAxiosApi...
function ParentComponent({ info }) {
const dataConfig = { season: info.season, scope: info.scope };
const [data, isLoading1, isError1] = useAxiosApi('this-endpoint', [], dataConfig);
return (
{isLoading && <p>We are loading...</p>}
{!isLoading &&
... use the data to render something...
}
)
}
useAxiosApi
import axios from 'axios';
import { useState } from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
const resources = {};
const useAxiosApi = (endpoint, initialValue, config) => {
// Set Data-Fetching State
const [data, setData] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// For Live Search on keystroke, Save Fetches and Skip Fetch if Already Made
if (endpoint === 'liveSearch' && resources[config.searchText]) {
return [resources[config.searchText], false, false];
}
// Otherwise, Continue Forward
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
console.log('isMounted: ', isMounted);
if (isMounted) {
setData(result.data);
}
// If LiveSearch, store the response to "resources"
if (endpoint === 'liveSearch') {
resources[config.searchText] = result.data;
}
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
};
// Call Function
fetchData();
// Cancel Request if needed in cleanup function
return () => {
console.log('Unmount or New Search? About to call source.cancel()');
isMounted = false; // is this doing its job?
source.cancel();
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
export default useAxiosApi;
createUrl is simply a function that takes the endpoint and dataConfig and creates the url that axios will fetch from. Note that our cancelTokens seem to be working in conjunction with the Live search, as new searches are cancelling the old search queries, and the saving of data results into resources for the one specific endpoint liveSearch works as well.
However, our problem is that when ParentComponent is unmounted quickly, before the data fetch is complete, we still receive the Cant perform a React state update warning. I've checked the console.logs(), and console.log('isMounted: ', isMounted) is always returning true, even if we unmount the component quickly after it is mounted / before data fetching is complete.
We're at a loss on this, as using the isMounted variable is the way that I've seen this problem handled before. Perhaps there's a problem with the useDeepCompareEffect hook? Or maybe we're missing something else.
Edit: Weve also tried to create the isMounted variable from inside of ParentComponent, and pass that as a parameter into the useAxiosApi function, however this did not work for us either... In general, it would be much better if we can handle this warning via an update to our useAxiosApi function, as opposed to in the ParentComponent.
Edit2: It seems like the cancelToken only works when a duplicate API call is fired off to the same endpoint. This is good for our liveSearch, however it means that all of the other fetches are not cancelled.

Categories

Resources