problem with using apollo client mutate outside render - reactjs

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

Related

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.

Can't perform a React state update on an unmounted component when using useEffect hook

i seem to be getting the following error when using useEffect hook.
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 believe it has something to do with the async function i am calling to set if the user is authenticated or not.
ProtectedRoute.tsx
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
isUserAuthenticated(context.token).then(setIsAuthenticated).catch(setIsAuthenticated);
});
if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
const isUserAuthenticated = async (token: any): Promise<boolean> => {
try {
const response = await VerifyAuthentication(token);
console.log("VerifyAuthentication", response);
if (response.status !== 200) {
return false;
}
return true;
} catch (err) {
return false;
}
};
App.tsx
class App extends Component<Props, State> {
renderRouter = () => {
return (
<Router>
<Switch>
<ProtectedRoute exact path="/" component={Dashboard} />
<Route exact path="/login" component={Login} />
</Switch>
</Router>
);
};
render(): ReactElement {
return (
<div className="App">
<AppProvider>
<Theme>
<Sidebar>{this.renderRouter()}</Sidebar>
</Theme>
</AppProvider>
</div>
);
}
}
Presumably this redirects the user to a route which doesn't have this component:
return <Redirect to={{ pathname: "login" }} />;
Which means the component is unmounted, or generally unloaded from active use/memory. And this always happens, because this condition will never be true:
if (isAuthenticated) {
Because when the component first renders that value is explicitly set to false:
const [isAuthenticated, setIsAuthenticated] = useState(false);
So basically what's happening is:
You fire off an asynchronous operation to check if the user is authenticated.
Before waiting for the response, you decide that the user is not authenticated and redirect them.
The component is unloaded because the user has left this page.
The asynchronous response is received and tries to update state for a component that is no longer loaded/mounted.
It's not entirely clear how this component is intended to fit into your overall structure, but you're going to need to change that structure. Either checking for authentication would need to be synchronous or you'd need to wait for the asynchronous operation to complete before redirecting. An example of the latter could be as simple as:
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
isUserAuthenticated(context.token)
.then(x => {
setIsAuthenticated(x);
setIsLoading(false);
})
.catch(e => {
setIsAuthenticated(false);
setIsLoading(false);
console.log(e);
});
});
if (isLoading) {
return <div>Loading...</div>;
} else if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
In that scenario a separate state value of isLoading is used to track whether the asynchronous operation is still taking place, so the component "waits" until the data is loaded before deciding to redirect the user or not.
But overall I don't see why the authentication check can't be synchronous. Something higher-level, such as a provider component that wraps the entire application structure within <App/>, can have this same logic above, essentially performing the async operation and keeping the result in state. Then that state can be provided via useContext or Redux or even just passing as props to all child components.
You shouldn't need to re-check for authentication over and over in child components. That's an application-level concern.
You can use a variable to check component is mount or unmount when call setIsAuthenticated
useEffect(() => {
let isMouted = true;
isUserAuthenticated(context.token)
.then((val) => isMouted && setIsAuthenticated(val))
.catch(setIsAuthenticated);
return () => {
isMouted = false;
};
});

Firebase authentication & Public / Private Routing - React

In my react application, I am trying to implement Public and Private routes with react-router-dom.
I am currently getting the authentication state from firebase.auth().onAuthStateChanged() function.
The problem is that since the firebase.auth().onAuthStateChanged() function is asynchronous, JSX block is rendered with authState of false first, and then firebase.auth().onAuthStateChanged() sets authState to true after the JSX is returned.
So, the authentication is true, but my react app stays at the sign-in page.
PublicRoute.js
const PublicRoute = ({ component: Component, restricted, ...rest }) => {
let authState = false;
getAuthState()
.then(user => {
if (user.uid) {
authState = true;
} else {
authState = false;
}
})
return (
<Route { ...rest } render={ props => (
authState && restricted
? <Redirect to="/" />
: <Component { ...props } />
) } />
)
};
export default PublicRoute;
getAuthState.js
export const getAuthState = () => {
return new Promise((resolve, reject) => {
const waitAuthStateChange = () => {
let currentUser = firebase.auth().currentUser;
if (currentUser === null) {
firebase.auth().onAuthStateChanged(user => currentUser = user);
setTimeout(waitAuthStateChange, 100);
} else {
resolve(currentUser);
}
}
waitAuthStateChange();
})
};
I don't know how to make re-render or re-return the JSX after the authentication is fetched by firebase.auth().onAuthStateChanged() listener.
Thank you in advance!!
You will need a React state.
To use it, you can use it with a hook called useState();
Example,
const [authState,setAuthState] = useState(false);
getAuthState()
.then(user => {
if (user.uid) {
setAuthState(true);
}
})
Your current authState is a normal javascript object, so React doesn't regard it as a change and therefore, won't re-render.
Change it to React state and then when you do a setAuthState() , React use it as a pivot for change and will render again.
Side Note: Calling getAuthState() by itself is not a good practice in React.
In React way, it should be put inside the useEffect() hook.
Reference for useState: https://reactjs.org/docs/hooks-state.html
Reference for useEffect: https://reactjs.org/docs/hooks-effect.html

Component state doesn't change after action dispatch

On startup I use useEffect to fetch some data from local storage. If the fetch was successful I want to send an action and change the state of the component. After dispatching the action, I see that the reducer received an action and returned the new state, but I don't see any change in the component when I try to log the new state. What can be the reason for this behaviour.
Here's the action.ts:
import {TOKEN_VALIDITY} from './actionTypes'
export const setTokenValidity = (isTokenValid: boolean) => ({
type: TOKEN_VALIDITY,
isTokenValid
})
Here's the reducer.ts:
const auth = (state = false, action: any) => {
// after dispatch I see this log in the console value is true
console.log('auth reducer action type is ' + action.type + ' value is ' + action.isTokenValid)
switch(action.type) {
case TOKEN_VALIDITY:
return action.isTokenValid
default:
return state
}
}
export default auth
In the component I want to update the state of isTokenValid, but I always get undefined for the value.
This is the component code
const Stack = createStackNavigator();
let userToken = null;
const App = (props:any) => {
useEffect(() => {
const bootstrapAsync = async () => {
try {
userToken = await retrieveData('Token',null);
SplashScreen.hide();
if(userToken != null) {
props.setTokenValidity(true)
}
// this logs undefined for props.isTokenValid -- why???
console.log("after token isValid: " + props.isTokenValid)
console.log('token ' + userToken);
} catch (e) {
// Restoring token failed
}
};
bootstrapAsync();
}, []);
return (
<NavigationContainer>
<Stack.Navigator >
{!props.isTokenValid ? (
<>
<Stack.Screen name="Login" component={Login} options={{ headerShown:false }}/>
<Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
</>
) : (
<Stack.Screen name="Home" component={Home} options={{ headerShown:false }}/>
)}
</Stack.Navigator >
</NavigationContainer>
);
};
const mapDispatchToProps = (dispatch:any) => ({
setTokenValidity: (isTokenValid:boolean) => dispatch(setTokenValidity(isTokenValid))
})
const mapStateToProps = (state:any) => ({
isTokenValid: state.isTokenValid
})
export default connect(mapStateToProps, mapDispatchToProps)(App);
In the first rendering, i.e. when the useEffect is fired and you call the method using props.setTokenValidity to set the token validity, the token gets set. However, the console also gets printed on the same rendering.
When the state gets updated and you get an updated one using the props.isTokenValid, this is the 2nd re-rendering(not the same rendering when useEffect was called) and the useEffect doesn't fire, therefore we don't see the console being printed with the new value.
If you for some reason want to log when isTokenValid is set, use another useEffect
useEffect(() => {
console.log("after token isValid: " + props.isTokenValid)
},[props.isTokenValid]);
Reducers return a state object so you might wanna try doing this:
switch(action.type) {
case TOKEN_VALIDITY:
return { ...state, isTokenValid: action.isTokenValid }
default:
return state
}
The way I solved it in my project, is by creating multiple useEffects.
In order to solve your problem you need to do 2 things:
You need to run bootstrapAsync()
You need to check after bootstrapAsync finishes for the data that was set into userToken
You have already did no.1 successfully, And your current problem is that your component does not updates when it receives new data, aka when userToken updates.
The solution:
Write another useEffect function which will be rendered 2 times: one time on component load(which we will ignore because the fetch isn't done yet) and another time when userToken value updates.
In order to avoid running our new useEffect on component load, we need to create a new state, which we will call allowNavigation.
allowNavigation prop will be set to true only after the fetch is complete.
Then, only when allowNavigation is set to true, we can check userToken and handle it properly.
The following code will help you:
const App = (props:any) => {
const [allowNavigate, setAllowNavigate] = useState(false);
useEffect(() => {
const bootstrapAsync = async () => {
try {
userToken = await retrieveData('Token',null);
SplashScreen.hide();
setAllowNavigate(true);
...
};
bootstrapAsync();
}, []);
useEffect(() => {
if (allowNavigate)
// this will now log the correct values
console.log("after token isValid: " + props.isTokenValid)
console.log('token ' + userToken);
}, [allowNavigate]);
...

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

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>
)
}

Resources