I have a problem regarding my withAuth HOC in my Next JS project. This should let users access some routes only if they are logged in.
The problem is that it needs like 1 or 2 seconds to process the request and in this time the private route is rendered, then it is replaced with the desired one (the one for sign in).
import { useRouter } from "next/router";
import { useEffect } from "react";
import { LaLoader } from "../components/LaLoader";
import { useMeQuery } from "../generated/graphql";
export const withAuth = (Component) => {
const AuthenticatedComponent = () => {
const router = useRouter();
const { data, loading } = useMeQuery()
useEffect(() => {
const getUser = async () => {
if (!data?.me) {
router.push('/signin');
}
if (!data?.me.isCompleted) {
router.push('/complete-profile')
}
};
getUser();
});
if (loading) {
return <LaLoader />
}
return data?.me ? <Component data={data} /> : null;
};
return AuthenticatedComponent;
};
The behaviour I want is: if the request is still processing (loading), on the page will be rendered a loader; if the user isn't logged in he will be redirected to the sign in page and if he's signed in the private component will be displayed.
Thank you in advance for your help!
I don't think the getUser function needs to be declared async since it doesn't appear to call any asynchronous code nor await anything. With the synchronous code I think you just need to wait for the loading state to clear and do the same check to redirect the user.
Example:
export const withAuth = (Component) => (props) => {
const router = useRouter();
const { data, loading } = useMeQuery();
useEffect(() => {
if (!loading) {
if (!data?.me) {
router.push("/signin");
} else if (!data?.me?.isCompleted) {
router.push("/complete-profile");
}
}
});
if (loading) {
return <LaLoader />;
}
return data?.me ? <Component {...props} data={data} /> : null;
};
Related
When I place my Provider component at the highest level of the application it does not update with the correct state. I did try placing console logs within it which do not show up either.
The code looks like this:
const Root = () => {
return (
<AuthProvider>
<App />
</AuthProvider>
);
};
AppRegistry.registerComponent('My App', () => Root);
It is immediately consumed in the child component "App" which shows it has a value of null.
If I remove it from the Root function shown above and instead nest it inside another component it works as intended.
export default function App() {
return (
<AuthProvider>
<PrimaryComponent/>
</AuthProvider>
);
}
**PrimaryComponent: **
export const PrimaryComponent = () => {
const authContext = useAuthContext();
//... Other code
}
For reference:
export const useAuthContext = () => {
return useContext(AuthContext);
}
In both instances I used a useContext(AuthContext) but only the later works.
I don't understand why when it is placed as the first thing React must render it does not trigger anything but when nested it works as normal.
Edit: Added the code where i use the context
const PrimaryComponent = () => {
const authContext = useAuthContext();
const navigation = useNavigation<any>();
useEffect(() => {
intialize();
}, []);
useEffect(()=> {
if(!authContext.authState.authenticated){
if(navigation.canGoBack()){
navigation.dispatch(StackActions.popToTop());
}
}
},[authContext]);
const intialize = async () => {
try {
const value: any = await SecureStore.getItemAsync('token');
const authData = JSON.parse(value);
if (authData === null || authData.accessToken === null) {
navigation.navigate('login');
return;
}
// Update state
authContext.setAuthState({
authenticated: authData.accessToken !== null,
});
// By default navigate to the home screen
navigation.navigate('home');
setStatus('success');
} catch (error) {
setStatus('error');
console.log(`Error: ${error.message}`);
authContext.setAuthState({
authenticated: false,
});
}
}
if (authContext.authenticated) {
return <UserNavigation />;
} else {
return <GenericNavigation />
}
}
I want to add the total number of clicks using the increment field value but when I try to open the link I created, the total clicks do not increase at all.
did I do something wrong?
import {useParams} from 'react-router-dom';
import { useEffect, useState } from 'react';
import React from 'react';
import { firestore, app } from '../../firebase';
import { CircularProgress, Box, Typography } from '#mui/material';
function LinkRedirect() {
const {shortCode} = useParams();
const [loading, setLoading] = useState(true)
useEffect(() => {
const fethLinkDoc = async () => {
const linkDoc = await firestore.collection('links').doc(shortCode).get();
if (linkDoc.exists){
const{ longURL, linkID, userUid} = linkDoc.data();
firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
} else {
setLoading(false)
}
}
fethLinkDoc()
}, [])
if (loading)
return (
<Box mt={10} textAlign="center">
<CircularProgress />
<Typography>Redirecting to the link</Typography>
</Box>
)
else return (
<Box mt={10} textAlign="center">
<Typography>Link is not valid</Typography>
</Box>
)
}
export default LinkRedirect
The problem is caused by these lines:
firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
Here you queue the document update operation to increment the counter, then immediately navigate away from the page, cancelling the operation.
Because you are using an async function, you can simply add await before the call to update():
await firestore.collection('users').doc(userUid).collection('links').doc(linkID).update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
However, I'd refactor your fethLinkDoc method to the following for readability:
const fethLinkDoc = async () => {
const linkDoc = await firestore.collection('links').doc(shortCode).get();
if (!linkDoc.exists) { // fail-fast
setLoading(false)
return
}
const { longURL, linkID, userUid } = linkDoc.data();
await firestore.collection('users')
.doc(userUid)
.collection('links')
.doc(linkID)
.update({
totalClicks: app.firestore.FieldValue.increment(1)
})
window.location.href = longURL;
}
Note: Your code currently doesn't properly handle when any of the firebase operations reject their promises. Either handle it manually or make use of an utility method like useAsyncEffect from a library.
I'm attempting to run a function within the useEffect hook, so that on screen load it automatically calls the context and works out what to do.
But for whatever reason, the function just isn't firing. The screen loads successfully and renders, no errors, but just doesn't do anything.
Here's my component I'm calling the context from:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { AuthContext } from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And my context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = () => {
return async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
}
};
return (
<AuthContext.Provider value={{ user, userSignedIn }}>
{ children }
</AuthContext.Provider>
)
Can anyone point out what I'm doing wrong? Feels a simple one.. But can't figure it out.
You are returning an async function when calling userSignedIn so the following should work for you by making userSignedIn itself async to work for those await calls inside.
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
};
I am practicing AWS' Cognito. For front-end I am using React and for routing I am using React-router-dom. For Cognito validation I am using amazon-cognito-identity-js package. My Congito signin, signup and confirmation logic works fine. I made one helper function where I validate the Congnito. and reuse it in different component. I split my Nav bar into two components. From Congnito current user I made one callback function and use it in useEffect, and dependencies put the callback function, by default getAuthenticatedUser is null. I add condition where it fetch the data, if getAuthenticatedUser then redirect to signin and signup page. Because of this condition I am getting the error: Can't perform a React state update on an unmounted component...... Also when I signed in it does not change the nav bar name, I have to refresh the browser then I can see the change. I share my code in codesandbox.
This is my helper function
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { CognitoUserPool } from 'amazon-cognito-identity-js';
const Pool_Data = {
UserPoolId: 'us-east-1_IEyFfUupx',
ClientId: '63fc9g5c3g9vhqdalrv9eqhoa2',
};
export default function useHandler() {
const [state, setstate] = useState({
loading: false,
isAuthenticated: false
})
const { loading, isAuthenticated } = state;
const userPool = new CognitoUserPool(Pool_Data)
const getAuthenticatedUser = useCallback(() => {
return userPool.getCurrentUser();
},
[],
);
console.log(getAuthenticatedUser());
useEffect(() => {
getAuthenticatedUser()
}, [getAuthenticatedUser])
const signOut = () => {
return userPool.getCurrentUser()?.signOut()
}
console.log(getAuthenticatedUser());
return {
loading,
isAuthenticated,
userPool,
getAuthenticatedUser,
signOut
}
};
This is my navigation
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import SigninLinks from './SigninLinks';
import SignoutLinks from './SignoutLinks';
import useHandlder from '../configHandler/useHandler';
const Nav = () => {
const { getAuthenticatedUser } = useHandlder();
const Links = getAuthenticatedUser() ? <SigninLinks /> : <SignoutLinks />
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<h2 className="brand-logo">Logo</h2>
{
Links
}
</div>
</nav>
);
};
export default Nav;
This is Home screen where it display the data and getting error
import React, { useState, useEffect } from "react";
import { api } from './api';
import useHandlder from './configHandler/useHandler'
import { Redirect } from 'react-router-dom';
const Home = () => {
const [state, setstate] = useState([]);
const { getAuthenticatedUser } = useHandlder();
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const data = await response.json();
setstate(data)
}
return getAuthenticatedUser() === null ? <Redirect to="/signin" /> : //In here is the //error happening.
<div className="row">
<h1>hello welcome to home</h1>
{
state?.map((i: string, id: number) => <h1 key={id}>{i.title}</h1>)
}
</div>
};
export default Home;
Issue
The issue is your app starts on the home ("/") path and renders the Home component. Home initiates a GET request upon mounting and checks for an authenticated user, and if there is none, renders a redirect to your "/signin" route.
The fetch is asynchronous so when the redirect occurs the GET request is resolving after Home has been unmounted and it tries to update the local state with the response data, but can't.
Solution
You need to use an Abort Controller to cancel in-flight requests. If the component unmounts, an effect cleanup function cancels the fetch request. In Home update the useEffect hook to create an AbortController and signal to be used in a cleanup function.
useEffect(() => {
const controller = new AbortController(); // <-- create controller
const { signal } = controller; // <-- get signal for request
const fetchData = async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{ signal } // <-- pass signal with options
);
const data = await response.json();
setstate(data);
};
fetchData();
return () => controller.abort(); // <-- return cleanup function to abort
}, []);
Demo
I have been trying to use a cleanup function to cancel the API call I a user presses the back button before the request is resolved.
However I still receive the same error "Warning: Can't perform a React state update on an unmounted component.".
I am using fetch function, I added the abortController but still I receive the same warning.
import React, { useState, useEffect, useReducer, useContext } from "react";
import { ActivityIndicator } from "react-native";
import AllThumbnails from "../components/AllThumbnails";
import reducer from "../functions/reducer";
import { lightColors, darkColors } from "../constants/Colors";
import { ThemeContext } from "../context/context";
import ScreenContainer from "../components/UI/ScreenContainer";
export default function AllCatScreen(props) {
const { navigation, route } = props;
const [categories, setCategories] = useState([]);
const [state, dispatch] = useReducer(reducer, { catPage: 1 });
const [theme] = useContext(ThemeContext);
const { taxonomy } = route.params;
useEffect(() => {
const abortCtrl = new AbortController();
const opts = { signal: abortCtrl.signal };
let isActive = true;
fetch(`${siteURL}/wp-json/wp/v2/${taxonomy.endPoint}`, opts)
.then((response) => response.json())
.then((res) => {
if (isActive) {
setCategories([...categories, ...res]);
}
})
.catch((err) => console.log(err));
return function cleanup() {
isActive = false;
console.log(isActive);
abortCtrl.abort();
};
}, []);
if (categories.length == 0) {
return (
<ScreenContainer notYet={true}>
<ActivityIndicator size="large" color={theme.colors.text} />
</ScreenContainer>
);
} else {
return (
<ScreenContainer notYet={false}>
<AllThumbnails
data={categories}
navigation={navigation}
catThumb={true}
action={[state, dispatch]}
fetchData={fetchData}
/>
</ScreenContainer>
);
}
}
I have read that react native should support the AbortController. I am using Expo SDK 38 but even in the clean up function logging the console doesn't work. Does anyone know what's wrong?