Private Route Conditional Operator in ReactJS - javascript

Seems like a trivial issue.
I have an app, where people can subscribe through "stripe". Would like to give access to a few URLs based on subscription, otherwise taking them back to the "profile" page.
A couple of things are not working.
Firebase query to get subscription is not giving results on the subscription. Somehow the onSnapShot does not fetch anything. Probably since UID is null at the start of rendering of page.
Conditional operator on is not working. Not sure what the problem is on this one.
function PaidRoutes(props) {
const [subscription, setSubscription] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const unsub = auth.onAuthStateChanged((authObject) => {
unsub();
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
let docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setLoading(false);
}
});
return () => {
unsub();
};
}, []);
return (
<Route
{...props}
render={(props) =>
subscription ? (
<Component {...props} />
) : (
<Redirect to='/profileScreen' />
)
}
/>
);
Thanks Drew for comment. The installed version of react-router-dom is 5.2.0

Issue(s)
Potential issues I see in the PaidRoute code:
The unsubscribe function is called in the auth state handler, this might allow the auth check to work once on an initial auth change, but then will unsubscribe itself and stop working until the component remounts.
The loading state is initially false so any check based on it on the initial render won't do what you want. The loading also isn't used to hold off on the conditional rendering.
The subscription state is initially an empty array which is still truthy, so the conditional logic checking it will likely allow access to the protected route anyway, regardless of any auth status.
Solution
Don't unsubscribe from the auth change listener in the callback.
Start with loading initially true to handle the initial render cycle. Conditionally render null or some loading indicator.
Start with null subscription state.
Code:
function PaidRoutes(props) {
const [subscription, setSubscription] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((authObject) => {
if (authObject) {
setLoading(true);
const uid = auth.currentUser?.uid;
console.log('UID ==>', uid);
const docRef = query(collection(db, 'customers', uid, 'subscription'));
console.log('DOC REF ==> ', docRef);
onSnapshot(docRef, (snap) => {
snap.forEach((doc) => {
console.log('Role of Subscription', doc.data().role);
setSubscription(doc.data().role);
});
});
} else {
console.log('not logged in');
setSubscription(null); // <-- reset auth state
}
setLoading(false); // <-- clear loading state outside if-else
});
return unsubscribe;
}, []);
if (loading) {
return null; // or loading indicator, spinner, etc...
}
return subscription ? (
<Route {...props} />
) : (
<Redirect to='/profileScreen' />
);
}

Related

React state not updating inside custom route

I am having an issue where my react state is not updating.
I am trying to make a role-based protected route, following this tutorial style https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5, using the following component:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(false);
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, [])
useEffect(() => {
console.log(role);
}, [role])
return role === 'ADMIN' ? children : <Navigate to="/" />;
}
Logging the role in the useEffect function displays an empty result in the console.
Logging the variable response directly after the await function displays the correct response retrieved from the server.
I've tried to console log the role directly after the checkAuth() function in useEffect(), but also obtained an empty line in the console.
What could be the problem?
This component is used as the following in App.js file:
<Route
element={
<MasterRoute>
<Dashboard child={<Admin />}></Dashboard>
</MasterRoute>
}
path={'/roles'}
></Route>
Issue
It seems the general problem is that the initial role state is '', and since '' === 'ADMIN' evaluates false the <Navigate to="/" /> is rendered and the route changes. In other words, the route changed and MasterRoute likely isn't being rendered when the checkAuth call completes.
Solution
You could use that isLoading state to conditionally render null or some loading indicator while the auth/role status us checked. You'll want MasterRoute to mount with isLoading initially true so no routing/navigation action is taken on the initial render cycle.
Example:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(true); // <-- initially true
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, []);
useEffect(() => {
console.log(role);
}, [role]);
if (isLoading) {
return null; // or loading indicator/spinner/etc
}
return role === 'ADMIN' ? children : <Navigate to="/" replace />;
}

React app keeps refreshing when changing i18next language in combination with authenticated routes

I have a react-app with authentication (cookie based) with a login route and a profile route. The profile route is fetching the profile data and puts the data in a form. One of the fields is a language field. I'm using the useEffect hook to watch the language property and use i18n.changeLanguage() to change the language.
For some reason the page keeps refreshing when I add this code. It must be a combination of this code together with the code I'm using the check if the user is authenticated to access the route. When I comment out the protectedRoute function or the useEffect hook it's working but I obviously need both.
A small breakdown of the protectedRoute function and authContext.
The routes are wrapped in an AuthProvider
const App = () => {
return (
<BrowserRouter>
<AuthProvider>
<Router />
</AuthProvider>
</BrowserRouter>
);
};
Inside the AuthProvider I have a user and isAuthenticated state. Both starting with a value of null. On mount a call with or without a cookie is done to the backend to get the user info. If a user object is returned with an id the isAuthenticated state is set to true.
const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
getUserInfo();
}, []);
const getUserInfo = async () => {
try {
const { data } = await authService.me();
setIsAuthenticated(true);
setUser(data);
} catch (error) {
setIsAuthenticated(false);
setUser({});
}
};
const setAuthInfo = (user) => {
setIsAuthenticated(!!(user && user.id));
setUser(user);
};
...
As long as isAuthenticated is null a loading state is rendered instead of a route.
if (authContext.isAuthenticated === null) {
return (
<div>
<span>Loading...</span>
</div>
);
}
const ProtectedRoutes = () => {
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace />
);
};
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoutes />}>
<Route path="/" element={<Navigate to="/profile" replace />} />
<Route path="/profile" element={<ProfileOverview />} />
</Route>
</Routes>
);
The profile page can be accessed when the isAuthenticated state is true. Inside the profile page the user profile information is fetched and with a reset set into the form state. This will trigger the useEffect hook watching the formData.language property which will set the language to the user's language. This leads to a continuous refresh and I can't find the reason why or what I'm doing wrong.
const Profile = () => {
const { i18n } = useTranslation();
const { formData, reset, handleSubmit, handleChange } = useForm({});
useEffect(() => {
console.log("Get profile data");
getProfileInfo();
}, []);
useEffect(() => {
i18n.changeLanguage(formData.language);
}, [formData.language]);
const getProfileInfo = async () => {
const { data } = await profileService.getProfileInfo();
reset(data);
};
const submit = (values) => {
console.log("submit");
};
...
Codesandbox demo over here. I have put a console.log inside the useEffect on the profile page so you can see that it keeps refreshing. Login can be done without credentials. All fetches are done with a setTimeout to fake real calls.
Taken from the Codesandbox demo and modified within Profile.js. The idea is to block additional requests to i18n.changeLanguage until the previous request has finished and the language was truly updated.
// Use a state variable to verify we aren't already loading
const [isLoadingLanguage, setLoadingLanguage] = useState(false);
useEffect(() => {
// Verify we aren't loading and are actually changing the language
if (!isLoadingLanguage && formData.language !== language) {
const load = async () => {
// Set the state as loading so we don't perform additional requests
setLoadingLanguage(true);
// Since this method returns a promise, and useEffect does not allow async/await easily,
// make and call an async method so we can await changeLanguage
await i18n.changeLanguage(formData.language);
// Originally, we wanted to set this again to update when the formData language updated, but we'll do in submit
// setLoadingLanguage(false);
};
load();
}
}, [i18n, formData.language]);
I would note, getting this to work in the sandbox required removing the line to i18n.changeLanguage so perpetual renders would not keep submitting requests. Then, adding back the useEffect above loaded the form and provided single submissions.
Edit: To prevent i18n.changeLanguage from rendering the component again we can do a few things.
Remove the setLoadingLanguage(false) from the useEffect so we don't trigger a language change until we submit
Add i18n.changeLanguage to the submit method AND localStorage.setItem("formData", JSON.stringify(formData) to retain the state when we do submit
const submit = (values) => {
localStorage.setItem("formData", JSON.stringify(formData));
i18n.changeLanguage(formData.language);
console.log("submit");
};
Retrieve any localStorage item for "formData" in place of the getProfileInfo
export default {
getProfileInfo() {
console.log({ localStorage });
const profileData =
localStorage.getItem("formData") !== null
? JSON.parse(localStorage.getItem("formData"))
: {
first_name: "John",
last_name: "Doe",
language: "nl"
};
const response = {
data: profileData
};
return new Promise((resolve) => {
setTimeout(() => resolve(response), 100);
});
}
};

Return from a funtion only when API returns a success response from backend in React

I need a help on regarding private routing in react-router-6.Actually, I want to return a component only when the backend API returns success response.
please see the following code snipet,
export default function PrivateOutlet() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
localStorage.setItem('id', res.id);
})
.catch(error => console.log(error) );
}, [);
return (localStorage.getItem('id') !== "undefined") ? <Outlet /> : <Navigate to="/login" />
};
Here, the problem is , before returning success response from the getCurrentUser() API this function PrivateOutlet returns value.
can you please help on this?
The localStorage seems completely extraneous/superfluous if what you really want is to use the getCurrentUser action to get an id. Use a local state id that doesn't match the authenticated or unauthenticated id value. Wait until the id state is populated to render the Outlet or redirect.
Example:
export default function PrivateOutlet() {
const [id, setId] = React.useState();
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
setId(res.id); // valid id or null
})
.catch(error => {
console.log(error);
setId(null);
);
}, []);
if (id === undefined) return null; // or loading indicator/spinner/etc
return (id)
? <Outlet />
: <Navigate to="/login" replace />
};
If wanting to continue using localStorage, then use a local "loading" state and conditionally render null or some loading indicator until the current user id is fetched. Also, I'm not sure if it was a typo, but you very likely meant to compare the value from localStorage against undefined and not the string literal "undefined". This still works because the loading state update triggers a rerender and the component can read the id value just set in localStorage.
Example:
export default function PrivateOutlet() {
const [isLoaded, setIsLoaded] = React.useState(false);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser())
.then((res) => {
localStorage.setItem('id', res.id);
})
.catch(error => {
console.log(error);
localStorage.removeItem('id');
)
.finally(() => setIsLoaded(true));
}, []);
if (!isLoaded) return null; // or loading indicator/spinner/etc
return (localStorage.getItem('id') !== undefined)
? <Outlet />
: <Navigate to="/login" replace />
};

Routing with Spotify API

I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])
You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}

React useEffect causing: Can't perform a React state update on an unmounted component

When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.
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."
Why do I keep getting this warning?
I tried researching these solutions:
https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
but this still was giving me the warning.
const ArtistProfile = props => {
const [artistData, setArtistData] = useState(null)
const token = props.spotifyAPI.user_token
const fetchData = () => {
const id = window.location.pathname.split("/").pop()
console.log(id)
props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
.then(data => {setArtistData(data)})
}
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [])
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
}
Edit:
In my api file I added an AbortController() and used a signal so I can cancel a request.
export function spotifyAPI() {
const controller = new AbortController()
const signal = controller.signal
// code ...
this.getArtist = (id) => {
return (
fetch(
`https://api.spotify.com/v1/artists/${id}`, {
headers: {"Authorization": "Bearer " + this.user_token}
}, {signal})
.then(response => {
return checkServerStat(response.status, response.json())
})
)
}
// code ...
// this is my cancel method
this.cancelRequest = () => controller.abort()
}
My spotify.getArtistProfile() looks like this
this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
return Promise.all([
this.getArtist(id),
this.getArtistAlbums(id,includeGroups,market,limit,offset),
this.getArtistTopTracks(id,market)
])
.then(response => {
return ({
artist: response[0],
artistAlbums: response[1],
artistTopTracks: response[2]
})
})
}
but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.
For me, clean the state in the unmount of the component helped.
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:
function Component(props) {
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
const ac = new AbortController();
Promise.all([
fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
]).then(() => setFetched(true))
.catch(ex => console.error(ex));
return () => ac.abort(); // Abort both fetches on unmount
}, []);
return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.
We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).
The code with fixes:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
const componentMounted = useRef(true); // (3) component is mounted
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
if (componentMounted.current){ // (5) is component still mounted?
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
}
return () => { // This code runs when component is unmounted
componentMounted.current = false; // (4) set it to false when we leave the page
}
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Why do I keep getting this warning?
The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.
How do I know if I have a memory leak?
You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.
It typically looks like this:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
// "store" lives longer than the component,
// and will hold a reference to the handleChange function.
// Preventing the component to be garbage collected after
// unmount.
store.subscribe(handleChange)
// Uncomment the line below to avoid memory leak in your component
// return () => store.unsubscribe(handleChange)
}, [])
Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:
useEffect(() => {
function handleScroll() {
setState(window.scrollY)
}
// document is an object in global scope, and will hold a reference
// to the handleScroll function, preventing garbage collection
document.addEventListener('scroll', handleScroll)
// Uncomment the line below to avoid memory leak in your component
// return () => document.removeEventListener(handleScroll)
}, [])
Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.
But that is not what I am doing, why should I care about this warning?
React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.
But it is so annoying to see the warning, how do I remove it?
There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.
The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.
In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.
You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
return () => setDidMount(false);
}, [])
if(!didMount) {
return null;
}
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
Hope this will help you.
I had a similar issue with a scroll to top and #CalosVallejo answer solved it :) Thank you so much!!
const ScrollToTop = () => {
const [showScroll, setShowScroll] = useState();
//------------------ solution
useEffect(() => {
checkScrollTop();
return () => {
setShowScroll({}); // This worked for me
};
}, []);
//----------------- solution
const checkScrollTop = () => {
setShowScroll(true);
};
const scrollTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
window.addEventListener("scroll", checkScrollTop);
return (
<React.Fragment>
<div className="back-to-top">
<h1
className="scrollTop"
onClick={scrollTop}
style={{ display: showScroll }}
>
{" "}
Back to top <span>⟶ </span>
</h1>
</div>
</React.Fragment>
);
};
I have getting same warning, This solution Worked for me ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
if you have more then one fetch function then
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
This error occurs when u perform state update on current component after navigating to other component:
for example
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
dispatch(login(res.data.data)); // line#5 logging user in
setSigningIn(false); // line#6 updating some state
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.
Solution:
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
setSigningIn(false); // line#6 updating some state -- moved this line up
dispatch(login(res.data.data)); // line#5 logging user in
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN
there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):
useEffect(() => {
// get abortion variables
let abortController = new AbortController();
let aborted = abortController.signal.aborted; // true || false
async function fetchResults() {
let response = await fetch(`[WEBSITE LINK]`);
let data = await response.json();
aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
if (aborted === false) {
// All your 'set states' inside this kind of 'if' statement
setState(data);
}
}
fetchResults();
return () => {
abortController.abort();
};
}, [])
Other Methods:
https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8
If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.
If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.
export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
let [parameters, setParameters] = useState(rest);
if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
setParameters(rest);
const refresh: () => void = useCallback(() => {
const promise: Promise<T | void> = gettor
.apply(null, parameters)
.then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
.catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
return promise;
}, [gettor, parameters]);
useEffect(() => {
refresh();
// and for when async finishes after user navs away //////////
return () => { setTuple = setParameters = (() => undefined) }
}, [refresh]);
let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
return tuple;
}
That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.
export const MyComponent = () => {
const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
// ..etc.
// imported from elsewhere ////
export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
const [val, setVal] = useStateTuple;
const [isMounted, setIsMounted] = useState(true);
useEffect(() => () => setIsMounted(false), []);
return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}
You could then create a useStateAsync hook to streamline a bit.
export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
return useUnlessUnmounted(useState(initialState));
}
Try to add the dependencies in useEffect:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
Usually this problem occurs when you showing the component conditionally, for example:
showModal && <Modal onClose={toggleModal}/>
You can try to do some little tricks in the Modal onClose function, like
setTimeout(onClose, 0)
This works for me :')
const [state, setState] = useState({});
useEffect( async ()=>{
let data= await props.data; // data from API too
setState(users);
},[props.data]);
I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:
Bad code (caused the error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
}
setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
}
Good code (no error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
setLoading(false) // moving this line INTO the catch call resolved the error!
}
}
Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:
useEffect(() => {
const fetchUser = async() => {
const {
data: {
queryUser
},
} = await authFetch.get(`/auth/getUser?userId=${createdBy}`);
setBlogUser(queryUser);
};
fetchUser();
return () => {
setBlogUser(null);
};
}, [_id]);
This improves upon Carlos Vallejo's answer.
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.
it work for me ....
The easy way
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
options={{
filterType: "checkbox"
,
textLabels: {
body: {
noMatch: isLoading ?
:
'Sorry, there is no matching data to display',
},
},
}}

Categories