Avoid update on an unmounted component - reactjs

I read a lot of similar questions but it seems my questions is a little different.
I am trying to login and I get the following error.
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.
I am attaching the AuthContext.js that handles all the logic hoping you can explain me what is wrong and what knowledge it indicates I need to learn.
import React, { useContext, useState } from 'react'
import {postData} from '../adapters/authAdapter';
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState(null);
async function login(email,password) {
const adaptedRes = await postData('log-in',{email,password});
if(adaptedRes.err) {
throw new Error(adaptedRes.message)
} else {
return setCurrentUser(adaptedRes.data);
}
}
const value = {
currentUser,
login
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Thank you in advance

The error means that your setCurrentUser() function is being called, when AuthProvider is not currently mounted. That's why, use useRef() to check if your AuthProvider is mounted, and then set state as mentioned in the link:
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState(null);
const isMounted = useRef(false);
useEffect(() => {
// Becomes true on component mount
isMounted.current = true;
// becomes false on unmount
return () => {isMounted.current = false;}
}, [])
async function login(email,password) {
const adaptedRes = await postData('log-in',{email,password});
if(adaptedRes.err) {
throw new Error(adaptedRes.message)
} else {
if(!isMounted.current) {
// Check if component is mounted
console.log('component is not mounted');
return;
}
return setCurrentUser(adaptedRes.data);
}
}
const value = {
currentUser,
login
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Update As Drew Pointed out:
Issue is as others say, the AuthProvider component is being unmounted before setCurrentUser is called. Instead of putting a band-aid fix in to keep the state update from occurring you should figure out why your auth provider isn't mounted. This is one of those provider components you typically want wrapping your entire app and mounted at all times. My guess is you've narrowed the scope of this auth provider too much to just around your auth component UI and then you navigate elsewhere and the provider is no longer available.
So, you should also check this, as if you are getting this error, then there is implementation problem somewhere.

Related

Can't stop React from updating the main component after state change

I'm having the following issue.
I have a component called "BackgroundService" who has a setInterval for requesting data from an API every 5 seconds. The received data from API is stored in "backgroundServiceResult" hook with useState, located in App and shared by a context provider.
_app.js:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
console.log("App reloaded")
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<BackgroundService/>
<Component {...pageProps} />
</AppContext.Provider>
)
}
BackgroundService.js:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const BackgroundService = () => {
const { getLatestSyncInfo } = api()
const { isDBSet, getJson } = OfflineStorage()
const appContext = useContext(AppContext);
const [alreadyNotified, setalreadyNotified] = useState(false)
useEffect(async () => {
const intervalId = setInterval(async () => {
// REQUIRE DATA FROM API STUFF, AND CAll:
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
return (
<></>
)
}
The problem is, every time the appContext.setBackgroundServiceResult is called from BackgroundService.js, the entire App component is re-rendered! so the "console log" in App is called, and all the components mounted again.
How can I store the received data from API through all my application without rendering again all from App?
Any way for solving this?
Thanks you
Your application is following expected behaviour, when state or props update the component will re-render.
There are many options you could use to prevent this from negatively affecting parts of your application.
useEffect could be used to only run code in child components when the component is initially mounted or when specific props or state change.
useMemo could be used to only recalculate values upon specific props or state change.
useCallback could be used to only recreate a function when specific props or state change.
In your specific case here it doesn't make sense to create the BackgroundService if it isn't going to render anything. Instead you should be creating a hook like this:
import { useState, useEffect, useContext } from "react"
import AppContext from '#/hooks/AppContext'
export const useBackgroundService = () => {
const appContext = useContext(AppContext);
// Also bear in mind that the `useEffect` callback cannot be `async`
useEffect(() => {
// the `async` over here is fine though
const intervalId = setInterval(async () => {
appContext.setBackgroundServiceResult(data or stuff);
}, 5000)
return () => clearInterval(intervalId);
}, [])
}
And then call it in your app as follows:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Don't worry about the console.log going off, it won't negatively affect your application. If you had to do something like sort a massive list at the top level of your app component you could do something like this:
const App = ({ Component, pageProps }) => {
const [backgroundServiceResult, setBackgroundServiceResult] = useState([false]);
useBackgroundService();
const sortedList = useMemo(() => pageProps.myList.sort(), [pageProps.myList]);
return (
<AppContext.Provider value={{ backgroundServiceResult, setBackgroundServiceResult }}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
Then the sortedList value would only update when it needs to and your updated backgroundServiceResult wouldn't cause that value to be recalculated.
In the same way you could make use of useEffect in the children components to make sure code only runs on initial mount and not on the components being re-rendered.
If you update your question to be more specific about what problems your App being rendered are causing we could come up with a better solution to tackle that specific issue.

Problem with asynchronous tasks in useEffect hook

I have an error like this:
index.js:1 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.
at ToDoAdder (http://localhost:3000/static/js/main.chunk.js:193:3)
here's my code:
import React, { useState } from 'react';
import { useEffect } from 'react';
import { Trash, Pen, CheckLg, XCircleFill } from 'react-bootstrap-icons';
import { Button } from 'react-bootstrap';
import store from '../../store';
function ToDoAdder({ data }) {
const [user, setUser] = useState({});
const { id, title, completed, userId } = data;
useEffect(() => {
fetchUsers();
}, [user]);
const fetchUsers = async () => {
const data = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const users = await data.json();
setUser(users);
return user;
};
const handleTodoRemove = () => {
console.log(id);
store.dispatch({ type: 'remove/todo', payload: { id } });
};
return (
// here goes some code
)
}
export default ToDoAdder;
Please help me to fix this problem
That's not an error, that's a warning.
As it says, your component unmounted (why, we can't tell, since we don't see how it's used) before the async call finished.
In your case, it doesn't even indicate a memory leak unlike the message insinuates.
If you want to get rid of the warning, you can guard the setUser call using a hook that checks whether the component is actually still mounted at that point. One such reusable hook is react-use's useMountedState.

React Hook SetState Causing Infinite Loop

Below is my code, I am attempting to call setState inside of my logout button as this button only renders when Auth0 isLoggedIn is true.
This seems to be causing an infinite loop, my guess is because the Auth0 API is being called
every time. I am following this tutorial: https://www.youtube.com/watch?v=35lXWvCuM8o
The only difference is I am attempting to call setState without an onClick function,
can anyone please explain to me the issue and a fix, thank you in advance!
I understand I can use localStorage to maintain a users Logged in Session, but I am doing this purely to learn React Hooks,
import React, { useState, useContext, useCallback } from "react";
import { useAuth0 } from "#auth0/auth0-react";
import { ValueContext } from "./ValueContext";
const LogoutButton = () => {
const [login, setLogin] = useContext(ValueContext);
const { logout, isAuthenticated } = useAuth0();
const updateLogin = () => {
setLogin({ loggedIn: "false" });
};
return (
isAuthenticated && (
<>
<button onClick={() => logout()}>Log Out</button>
<h1>{login.loggedIn}</h1>
{updateLogin()}
</>
)
);
};
You're calling updateLogin() inside of your render function, so you're setting loggedIn: true every render. Don't put functions or console logs directly inside of the return function, put them inside of the main LogoutButton function body if you must. But updateLogin() should only be called inside of some onPress.
You call updateLogin() every time the component renders. This sets the state variabel loggedIn. This will cause the component to render again and the process keeps looping.
The only difference is I am attempting to call setState without an onClick function
This is a big difference! setState should not be called in render function's main body, or at least, call it conditionaly. Otherwise you will get setState -> render -> setState -> render ->... it's endless.
You can try this:
let alreadySet = false;
const LogoutButton = () => {
const [login, setLogin] = useContext(ValueContext);
const { logout, isAuthenticated } = useAuth0();
const updateLogin = () => {
setLogin({loggedIn:'false'});
};
if(!alreadySet){
alreadySet = true;
updateLogin()
}
return (
isAuthenticated && (
<>
<button onClick={() => logout()}>
Log Out
</button>
<h1>{login.loggedIn}</h1>
</>
)
Notice this
let alreadySet = false;
....
{
if(!alreadySet){
alreadySet = true;
updateLogin()
}
}
It means loggedIn will only be updated once. Is it what you want? If it's not. You can updated your question with more details :)
If you need to use updateLogin when isAuthenticated is true, you can add useEffect hook.
like this
useEffect(()=>{ if (isAuthenticated){
setLogin({loggedIn:'false'});
}},[isAuthenticated, setLogin] );

What is causing Error: Too many re-renders in this code?

Can anybody please look at the following ReactJS component and tell what is causing it to return the error:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
import React, { useState, useEffect } from 'react';
function Lab() {
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
addQuestion('What is your name?');
addQuestion('Where do you belong?');
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Edit:
I can use some default values in useState([]), but that would make my code much messy because the data structure is quite complicated. That's why I want to push default values from within a helper function. Isn't it possible this way?
PROBLEM
Lab function is executed.
addQuestion is executed which triggers a re-render
Re-render triggers another execution of addQuestion
and thereby causes an infinite loop of re-renders and execution of addQuestion.
SOLUTION
add your default question as the default state in your Lab component.
import React, { useState, useEffect } from 'react';
const defaultQuestions = [
'What is your name?', 'What is your name?'
]
function Lab() {
const [questions, setQuestions] = useState(defaultQuestions);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
return (
<div>
{
questions.map( q => <div>{q}</div>)
}
</div>
);
}
export default Lab;
Whenever the state update, the whole function component will re-run again.
Therefore, in your code, when the questions variable is updated, the execution of the addQuestion function will be called again, and the function itself will update the state again, and that causes the infinite loop.
To prevent this kind of situation, it's better to let an event trigger the function.
Edit
If adding some default values is your main purpose, this is how you can do it:
import React, { useState, useEffect, useRef } from 'react';
function Lab() {
const isDefaultValueLoaded = useRef(false);
const [questions, setQuestions] = useState([]);
const addQuestion = (question) => {
let q = [...questions];
q.push(question);
setQuestions(q);
}
// To set the default values:
// use an useEffect hook to load the values when the component just mount.
useEffect(() => {
// to prevent the infinite loop,
// use a ref object as a flag to make sure the function will only run once.
if (isDefaultValueLoaded.current === false) {
loadDefaultValue();
isDefaultValueLoaded.current = true;
}
}, []);
// by writing the code above, you can now seperate the logic into an "helper function" as you mentioned.
const loadDefaultValue = () => {
addQuestion('What is your name?');
addQuestion('Where do you belong?');
}
return (
<div>
{
questions.map(q => <div>{q}</div>)
}
</div>
);
}
export default Lab;

React Hook useEffect dependency issue

I'm getting a warning message on my app and I've tried lots of things to remove it, without success. Error Message:
React Hook useEffect has a missing dependency: 'updateUserData'.
Either include it or remove the dependency array
react-hooks/exhaustive-deps
I don't want to exclude that with a comment to avoid the issue, but I want to fix it in a "best practices" way.
I want to call that updater function and get my component updated, so I can share that context in other components.
So... what i'm doing wrong? (any code review about the rest is very welcomed!)
Thanks a million!
If I add [] as the 2nd parameter of useEffect I get the warning, and removing it I get an inifinite loop.
Also adding [updateuserData] gets an infinite loop.
import React, { useState } from "react";
import UserContext from "./UserContext";
interface iProps {
children: React.ReactNode
}
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData: (id : string) => {
// call 3 services with axios in parallel
// update state of the 3 hooks
}
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
const UserPage: React.FC<ComponentProps> = (props) => {
const {data : {user, profile, questions}, updateUserData}: any = useContext(UserContext);
useEffect(() => {
// update information
updateUserData("abcId")
}, []);
return <div>...</div>
}
The idea is the following:
I have a context
I created provider for that content
that context exposes the data and an updater function
I use that provider in a component with the useEffect hook and I get the warning
I want to keep all the logic about fetching and updating the context inside the provider, so I don't replicate it all over the other components that are needing it.
Firstly, the infinite loop is caused by the fact that your context is updating, which is causing your component to be re-rendered, which is updating your context, which is causing your component to be re-rendered. Adding the dependency should prevent this loop, but in your case it isn't because when your context updates, a brand new updateuserData is being provided, so the ref equality check detects a change and triggers an update when you don't want it to.
One solution would be to change how you create updateUserState in your UserProvider, using e.g. useCallback to pass the same function unless one of the dependencies changes:
const UserProvider: React.FC<iProps> = (props) => {
// practice details
const [userState, setUserState] = useState({
id'',
name: ''
});
// practice skills
const [userProfileState, setuserProfileState] = useState([]);
// user selection
const [userQuestionsState, setuserQuestionsState] = useState({});
const updateuserData = useCallback(id=>{
// call your services
}, [userState, userProfileState, userQuestionsState])
return (
<UserContext.Provider value={{
data: {
user: userState,
userProfile: userProfileState,
questions: userQuestionsState
},
updateuserData
}}
>
{props.children}
</UserContext.Provider>
);
};

Resources