Apollo client (react) - Cant update state on unmounted component - reactjs

im trying to implement social authentication in my project and im getting this 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.
in FaceookSignIn (created by Socials)
...
Component in question recieves code from facebook which is put into url, for redirecting.
This is the route:
<PublicRoute exact path='/:callback?' component={Auth}/>
defined as:
export const PublicRoute = ({component: Component, ...rest}) => {
const {client, loading, data} = useQuery(GET_USER, {fetchPolicy: 'cache-only'})
let isAuthenticated = !!data.user.accessToken
return (
<Route {...rest} component={(props)=>(
isAuthenticated ? (
<Redirect to='/home' />
) : (
<Component {...props} />
)
)}/>
)
}
I've tried using hook cleanup on my component but error persists. This is what my current implementation looks like:
const FaceookSignIn = () => {
let _isMounted = false
const client = useApolloClient()
const appId = '187856148967924'
const redirectUrl = `${document.location.protocol}//${document.location.host}/facebook-callback`;
const code = (document.location.pathname === '/facebook-callback') ? querystring.parse(document.location.search)['?code'] : null
const [loading, setLoading] = useState(false)
const [callFacebook, data] = useMutation(FACEBOOK_SIGN_IN)
useEffect(()=>{
_isMounted = true
if(!code) return
if(_isMounted) callFacebook({variables: {code: code}})
.then(res=>{
const {error, name, email, accessToken} = res.data.facebookSignIn
if (error) {
alert(`Sign in error: ${error}`);
} else {
client.writeData({
data: {
user: {
name: name,
email: email,
accessToken: accessToken,
__typename: 'User'
}
}
})
setLoading(false)
}
})
.catch(e=>{
console.log(e)
setLoading(false)
})
return ()=> _isMounted = false
},[])
const handleClick = e => {
setLoading(true)
e.preventDefault()
window.location.href = `https://www.facebook.com/v2.9/dialog/oauth?client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUrl)}`;
}
return (
<a className="login-options__link" href='/facebook-login' onClick={handleClick}>
{loading ? <p>loading...</p> : <img className="social-link__icon" src={fb.default} id="facebook" /> }
</a>
)
}
This approach somewhat works, credentials are loaded and user is redirected to authenticated route but console still throws that error and ui is sometimes flicker between routes. Ive spent last two days on this and im out of ideas. Am i missing something obvious?

Ok, finally figured it out, turns out I shouldn't hijack logic from apollo hooks and be careful of how data is handled. I assume mutation hook updates client state on its own, .then() block resolved before client update and unmounted component. Maybe someone can clarify?
Anyway here is updated code if anyone is interested:
const FaceookSignIn = (props) => {
const appId = '187856148967924'
const redirectUrl = `${document.location.protocol}//${document.location.host}/facebook-callback`
const code = (document.location.pathname === '/facebook-callback') ? querystring.parse(document.location.search)['?code'] : null
//moved data handling logic to hooks optional callback
const [callFacebook, {client, data, loading, error, called}] = useMutation(FACEBOOK_SIGN_IN, {onCompleted: (data)=>{
const {name, email, accessToken} = data.facebookSignIn
client.writeData({
data: {
user: {
name: name,
email: email,
accessToken: accessToken,
__typename: 'User'
}
}
})
}})
if(code && !called) {
callFacebook({variables: {code: code}})
}
const handleClick = e => {
e.preventDefault()
window.location.href = `https://www.facebook.com/v2.9/dialog/oauth?client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUrl)}`;
}
return (
<a className="login-options__link" href='/facebook-login' onClick={handleClick}>
{loading ? <p>loading...</p> : <img className="social-link__icon" src={fb.default} id="facebook" /> }
</a>
)
}

Related

Component shows for a brief moment before redirecting in react router v6

I have a small issue I'm not able to fix. In my react app I use react-router v6 and I have to following routes:
<Route path="/" element={<App />} />
<Route path=":id" element={<CharacterDetails/>} />
<Route path="*" element={<Navigate replace to="/" />} />
As you can see, I have a route that needs an id. This works fine as long as I provide an existing Id so that CharacterDetails component fetches some data successfully. However, if I pass some random text in the URL like "localhost:3000/randomText" the CharacterDetails component still shows for a brief second till the useEffect fires to determine that the request is a 404 and only after that it redirects me to the App component.
How can I check if the URL provided should indeed return some data before rendering the component ? and redirect to the App component directly (without the flickering of the CharacterDetails component) when it is not a valid URL
Thanks!
EDIT: I'm not sure if this is a router issue or should I do it at the component level, I'm waiting for suggestions
EDIT2: Component code
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes, loading } = state;
useEffect(() => {
const fetchData = async (id: string) => {
dispatch({ type: "LOADING_START" })
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
dispatch({ type: "LOADING_OVER" })
} catch (error) {
dispatch({ type: "LOADING_OVER" })
navigate("/");
}
};
if (id) fetchData(id);
}, [id, navigate]);
return (
<CharacterDetailsContainer>
{loading ? <Loading /> :
data && (
<div> </div>
)}
</CharacterDetailsContainer>
}
You can use the useParams hook in the child.
const acceptableIDs = ["dog", "cat"];
function CharacterDetails() {
let { id } = useParams();
return acceptableIDs.includes(id) ? (
<div>
<h3>ID: {id}</h3>
</div>
) : null; // render nothing or redirect
}
If it takes too long to check if the ID is valid, you could show a transition.
Note this is business logic and should probably not bleed into the router.
This isn't an issue with the router/routes, it's something that routed components need to handle.
In the CharacterDetails component use some "loading" state to conditionally render null or some loading indicator while the id path param is validated. Note that "loading" needs to be the initial state so the code isn't leaking any initial non-loading UI, waiting until the useEffect hook runs at the end of the initial render to dispatch({ type: "LOADING_START" }) is too late unless the initial redux state has it set true.
Example:
const CharacterDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
setIsLoading(true);
// logic to validate id param
if (is404) {
navigate("/404", { replace: true }); // redirect
} else {
setIsLoading(false); // clear loading state so page content renders
}
}, [id]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return page content
};
Your code:
const CharacterDetails = () => {
const { id } = useParams<string>();
const navigate = useNavigate();
const [isLoading, setIsLoading] = React.useState<boolean>(true); // <-- initially true
const [state, dispatch] = useReducer(characterReducer, initialState);
const { data, episodes } = state;
useEffect(() => {
const fetchData = async (id: string) => {
setIsLoading(true);
dispatch({ type: "LOADING_START" });
try {
let response = await getSingleCharacterData(id);
let URLs = response.data.episode;
let listOfEpisodes = await getEpisodeName(URLs);
dispatch({
type: "FETCH_SUCCESS",
payload: { data: response.data, episodeList: listOfEpisodes },
});
setIsLoading(false);
} catch (error) {
// handle any errors, etc...
// redirect home
navigate("/", { replace: true });
} finally {
dispatch({ type: "LOADING_OVER" });
}
};
if (id) fetchData(id);
}, [id, navigate]);
if (isLoading) {
return null; // or loading spinner/etc...
}
return ( <some JSX> )
}

How to run use effect in App.js while using react router dom

I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.

My Login component flashes whenever I reload my dashboard

I'm using reactjs to build a login/register system with authentication and authorization. if authenticated(jsonwebtoken), it should route me to the dashboard else redirect me back to login.
but whenever I reload it hits the login endpoint for a second then back to dashboard. how can I fix this?
Below is a giphy to show what I'm talking about
Here are the components associated with the issue stated above
App.js
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
// set isAuthenticated to true or false
const setAuth = (boolean) => {
setIsAuthenticated(boolean)
}
useEffect(() => {
// check if the person is still Authenticated
const isAuth = async () => {
try {
const res = await fetch('/auth/verify', {
method: 'GET',
headers: { token: localStorage.token},
})
const data = await res.json()
// if authenticated, then
if(data === true) {
await setIsAuthenticated(true)
} else {
await setIsAuthenticated(false)
}
} catch (err) {
console.error(err.message)
}
}
isAuth()
})
return (
<Fragment>
<Router>
<div className='container'>
<Switch>
<Route exact path='/login' render={props => !isAuthenticated ? <Login {...props} setAuth={setAuth} /> : <Redirect to='/dashboard' /> } />
<Route exact path='/register' render={props => !isAuthenticated ? <Register {...props} setAuth={setAuth} /> : <Redirect to='/login' />} />
<Route exact path='/dashboard' render={props => isAuthenticated ? <Dashboard {...props} setAuth={setAuth} /> : <Redirect to='/login' /> } />
</Switch>
</div>
</Router>
</Fragment>
);
Login Component
const Login = ({ setAuth }) => {
const [text, setText] = useState({
email: '',
password: ''
})
const { email, password } = text
const onChange = e => setText({ ...text, [e.target.name]: e.target.value})
const onSubmit = async (e) => {
e.preventDefault()
try {
// Get the body data
const body = { email, password }
const res = await fetch('/auth/login', {
method: 'POST',
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body)
})
const data = await res.json()
if(data.token) {
// save token to local storage
localStorage.setItem("token", data.token)
setAuth(true)
toast.success('Login Successful')
} else {
setAuth(false)
toast.error(data)
}
} catch (err) {
console.error(err.message)
}
}
return (
<Fragment>
<h1 className='text-center my-5'>Login</h1>
<form onSubmit={onSubmit}>
Dashboard Component
const Dashboard = ({ setAuth }) => {
const [name, setName] = useState('')
useEffect(() => {
const getName = async () => {
try {
const res = await fetch('/dashboard', {
method: 'GET',
// Get the token in localStorage into the header
headers: { token: localStorage.token }
})
const data = await res.json()
setName(data.user_name)
} catch (err) {
console.error(err.message)
}
}
getName()
// eslint-disable-next-line
}, [])
// Log out
const logOut = (e) => {
e.preventDefault()
localStorage.removeItem("token")
setAuth(false)
toast.success('Logged Out')
}
return (
<Fragment>
<h1 className='mt-5'>Dashboard</h1>
<p>Hello, {name}</p>
<button className='btn btn-primary my-3' onClick={e => logOut(e)}>Log Out</button>
</Fragment>
There are two problems that I found in your code above.
The first is that your ueEffect does not specify any dependency.
When the dependencies are not specified in this way the useEffect would run anytime any state changes.
useEffect(()=> {
// code here
}); // this one would run anytime any state changes in the component. You usually don't want this.
When a dependency array is specified, the code in the useEffect would run anytime any of the state in the dependencies changes.
useEffect(()=> {
// code here
},
[state1, state2, ...others...] //code would run when any of the state in this array changes
In your case, however, you probably want to run that useEffect once. To do this we add an empty array as the dependency value.
useEffect(()=> {
// code here
},
[] //empty deps means that the code runs only once. When the component mounts
)
Extra ideas
I also suggest that you add a loading state to your component so that you can show a loader while the API call is being made.
You might want to show a loader while the API call is being made(or even set this state to true by default since the API call is the first thing you do in your app)
.
Also, consider putting useEffect in a custom Hook

Why is this React Component rendering first?

I could use your input on a quick question about Component loads.
The Goal
Return the <Login /> Component if the user isn't logged in, and the App if they are.
Expected Behavior
When a user is logged in, they see the App.
Observed Behavior
The <Login /> Component flickers (renders) for a moment, then the user sees the App.
My goal is to eliminate this flicker!
Code Samples
Index.js
export default function Index() {
let [isLoading, setIsLoading] = useState(true)
const router = useRouter()
// User object comes in from an Auth Context Provider
const { user } = useContext(AuthContext)
const { email } = user
useEffect(() => {
if (user) {
setIsLoading(false)
}
}, [])
// Returns the App if logged in, login screen if not
const getLoggedIn = () => {
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
}
return (
<Box className="App">
{ isLoading
? <div className={classes.root}>
<LinearProgress />
</div>
: getLoggedIn()
}
</Box>
)
}
Auth Context
Note: I'm using Firebase for auth.
// Listens to auth state changes when App mounts
useEffect(() => {
// Calls setUser state update method on callback
const unsubscribe = onAuthStateChange(setUser)
return () => {
unsubscribe()
}
}, [])
// Brings data from auth to Auth Context user state via callback
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
Stack
"next": "^8.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
Thanks so much for taking a look.
I had this exact problem and resolved it by storing the user in local storage
then on app start up do this:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')))
and it'll use the details from localstorage and you wont see a flicker
(it's because onauthstate takes longer to kick in)
So I figured out a sort of 'hacky' way around this. One needs to set the value of the boolean on which the initial load of the App depends...
const getLoggedIn = () => {
// Right here
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
...before making any asynchronous calls in the AuthContext. Like this:
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
// sets loggedIn to true to prevent flickering to login screen on load
callback({ loggedIn: true })
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
I hope this helps someone.

problem with using apollo client mutate outside render

I want to have a custom component named AuthRoute not only to check if certain data is available in redux but also verify token with graphql
here is what I've done :
const AuthRoute = ({ component: Component, authUser, ...rest }) => {
const you_must_go_back_to_login = <Route
{...rest}
render={props =>
(<Redirect to={{
pathname: '/user/login',
state: { from: props.location }
}}
/>)
}
/>
const continue_journey = <Route
{...rest}
render={props =>
(<Component {...props} />)
}
/>
const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
useEffect(() => {
client.mutate({
mutation: VERIFY_TOKEN_MUTATION,
variables: { token }
}).then(result => {
// setDest(continue_journey)
setCheckThat(true)
})
return () => {
console.log()
};
}, [])
// if(authUser) {
if (checkThat) {
return continue_journey
} else {
return you_must_go_back_to_login;
}
}
here is the steps I need them to be done:
run mutate
setDest to one of you_must_go_to_login or continue_journey
setCheckThat or something like that to true or false based on token verified or not
return dest as a result
now I've got this 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.
UPDATE
I changed my code to this but I still get the same error
const AuthRoute = ({ component: Component, authUser, ...rest }) => {
const you_must_go_back_to_login = (<Route
{...rest}
render={props =>
(<Redirect to={{
pathname: '/user/login',
state: { from: props.location }
}}
/>)
}
/>)
const continue_journey = (<Route
{...rest}
render={props =>
(<Component {...props} />)
}
/>)
// const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
useEffect(() => {
let isSubscribed = true
if (isSubscribed) {
getToken();
}
return () => isSubscribed = false
}, []);
const getToken = async (sig) => {
const data = await mutate(VERIFY_TOKEN_MUTATION, { token })
console.log(data)
setCheckThat(true);
console.log(checkThat)
return;
};
return checkThat ? continue_journey : you_must_go_back_to_login;
}
Error is caused by rendering redirect earlier than getting data.
Normlly ... instead useEffect you can use simple useQuery and use condition loading or !data for rendering some <Loading/> befor decision to redirect or give access (rendering redirect or guarded content).
Mutation is generally used for changing remote data. By using query you can pass variable and return answer, too.
I'm using django graphql jwt and the problem is that verifytoken for that is a mutation
In general/usually token is passed by header for requests and API returns response or errors (for missing/expired token). Usually you have an option to query current user for being logged verification.
... in this case we want to use mutation
... instead client.mutate we can use hooks - useMutation or better API example ( apollo docs :] ) - to get access to loading state before making decision
const [verifyToken, { data, loading, error }] = useMutation( VERIFY_TOKEN_MUTATION, variables: { token } );
Problems?
- mutations are not called at start - loading won't be true on the beggining;
- returned data.
For firing mutation at start we can use useEffect, for returned data we can use onCompleted handler but we can simply use data - should be undefined/nulled at start, usable for conditions.
Something like this should work (we don't need await etc. - data, loading, error will be updated automatically):
useEffect(() => {
verifyToken(); // ariables passed earlier, can be passed here
}, []);
if( !data ) return <Loading />
return data.someSuccessVerifiedField ? continue_journey : you_must_go_back_to_login;
The warning is pretty self-explanatory -
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.
Specially this part -
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
You are using useEffect in a wrong way. You should not put your mutation inside useEffect. What you can do is - take the mutation outside of the useEffect.
const [dest, setDest] = useState(you_must_go_back_to_login)
const [checkThat, setCheckThat] = useState(false)
client.mutate({
mutation: VERIFY_TOKEN_MUTATION,
variables: { token }
}).then(result => {
// setDest(continue_journey)
setCheckThat(true)
})
return () => {
console.log()
};
useEffect(() => {
// if(authUser) {
if (checkThat) {
return <continue_journey/>
} else {
return <you_must_go_back_to_login/>;
}
}, [checkThat])
}
So now mutation will run, then set checkThat variable. And then useEffect will be triggered which will return based on the value of checkThat

Resources