useContext causing unwanted re-render - javascript

I am having trouble with rerenders and memory leaks in my login form. The goal is to have a component that checks if the context's JWT is valid and if so redirects. However, when logging in and updating the context, the context causes a rerender when it should redirect anyways. What is the solution to this?
Edit: The issue seems to be that I am rerendering on authentication twice: once in Login and one in the SecuredRoute. Is there a more elegant solution?
useValidateToken.js
export default token => {
const [validateLoading, setLoading] = useState(true);
const [authenticated, setAuthenticated] = useState(false);
useEffect(() => {
fetch(`/validate_token`, {
method: "GET",
headers: { Authorization: "Bearer " + token }
})
.then(resp => {
if (resp.ok) setAuthenticated(true);
setLoading(false);
})
.catch(_ => setLoading(false));
}, [token]);
return { validateLoading, authenticated };
};
Login.js
function Login(props) {
const {token, setToken} = useContext(TokenContext)
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { isLoading: validateLoading, authenticated } = useValidateToken(token);
const [shouldRedirect, setShouldRedirect] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isInvalid, setIsInvalid] = useState(false);
function login() {
setIsLoading(true);
fetch(`/login`, { ... })
.then(resp => resp.json())
.then(body => {
if (body.jwt) {
setToken(body.jwt);
setShouldRedirect(true);
} else {
setIsInvalid(true);
setTimeout(function () { setIsInvalid(false) }, 3000)
setIsLoading(false);
}
})
.catch(_ => setIsLoading(false));
}
return validateLoading ? (
// Skipped code
) : shouldRedirect === true || authenticated === true ? (
<Redirect to={props.location.state ? props.location.state.from.pathname : "/"} />
) : (
<div className="login">
// Skipped code
<LoginButton loading={isLoading} login={login} error={isInvalid} />
</div>
</div>
);
}
The Route is secured using a custom component. This is done to secure routes and redirect to Login if there is an invalid token.
App.js
// Skipped code
const [token, setToken] = useState(null);
const { authenticated } = useValidateToken(token)
//Skipped code
<SecuredRoute exact path="/add-issue/" component={AddIssue} authenticated={authenticated} />
function SecuredRoute({ component: Component, authenticated, ...rest }) {
return (
<Route
{...rest}
render={props =>
authenticated === true ? (
<Component {...props} {...rest} />
) : (
<Redirect to={{ pathname: "/login", state: { from: props.location } }} />
)
}
/>
);
}

Related

React context provider updates state after context consumer renders

I am trying to implement Protected Routes in my app. I am using cookie-based session authentication.
The issue is: Whenever I try to access a protected page for the first time, the RequireAuth component has the isAuthenticated value as false and hence it navigates to /.
From the console logs, I can see Inside require auth. before Inside provide auth..
Questions:
Is using useEffect in the context provider the right way to set the auth state?
How do I make sure that the context provider state is set before accessing the context in the consumer RequireAuth?
I have a context provider ProvideAuth which makes an API call to check if the user is already authenticated.
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
Auth context consumer
export const useAuth = () => {
return useContext(authContext);
};
I use the context in a RequireAuth component to check if the user is already authenticated and redirect if not.
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo } = useAuth();
const location = useLocation();
useEffect(() => {
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, []);
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
The context provider is used in the App.js
return (
<ProvideAuth>
<div className='App'>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<Route element={<RequireAuth /> }>
<Route path="/jobs" element={<Jobs />} >
<Route index element={<MyJobs />} />
<Route path="new" element={<NewJob />} />
<Route path=":jobId" element={<JobDetails />} />
<Route path=":jobId/stats" element={<JobStats />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</ProvideAuth>
);
That's because that useEffect in ProvideAuth is as any useEffect an asynchronous task, which means the component and its children may render before its callback gets executed.
A solution is to set up a loading state in ProvideAuth, called for example isCheckingAuth, set to true by default, and to false after you have done all the fetching. And you pass it down to RequireAuth, like so :
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
setIsCheckingAuth(false)
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo,
isCheckingAuth
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
You use that isCheckingAuth inRequireAuth to show a loader while the fetching is being done, this way:
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo, isCheckingAuth } = useAuth();
const location = useLocation();
useEffect(() => {
if(isCheckingAuth) return;
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, [isCheckingAuth]);
if(isCheckingAuth) return <div>Loading...</div>
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
What you can do is check, If the request is processed or not. If processing show loader if any error shows some error msg or redirect. If everything is fine load provider.
const authContext = createContext();
export function ProvideAuth({ children }) {
const [state, setState] = useState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const isAuthenticated = await CheckAuthentication();
if (isAuthenticated) {
const user = await GetUserInfo();
setState((prev) => ({ ...prev, isAuthenticated, user }));
}
} catch (error) {
setState((prev) => ({ ...prev, error }));
} finally {
setState((prev) => ({ ...prev, isLoading: false }));
}
};
fetchData();
}, []);
if (state.isLoading) return <Loading />;
if (state.error) return <ErrorMessage error={state.error} />;
return <authContext.Provider value={state}>{children}</authContext.Provider>;
}

React native context api not passing value properly, null is not an object (evaluating 'user.uid')

I want to retrieve a field value of a document in Users collection by referencing it via the where condition from Firestore. I use the context api to pass the user object of the logged in user in my app. I get this error that user.uid is null. I can't spot where the mistake is. I have added the relevant piece of code.
EditProfile.js
const EditProfile = () => {
const { user } = React.useContext(AuthContext);
const [name, setName] = React.useState();
React.useEffect(() => {
const userid = user.uid;
const name = getFieldValue("Users", userid);
setName(name);
}, []);
};
export default EditProfile;
passing and getting value via context
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = React.useState(null);
return (
<AuthContext.Provider
value={{
user,
setUser,
}}
>
{children}
</AuthContext.Provider>
);
};
const AppStack = () => {
return (
<AuthProvider>
<BottomTab.Navigator>
<BottomTab.Screen
name="ProfileStack"
component={ProfileStack}
/>
</BottomTab.Navigator>
</AuthProvider>
);
};
export default AppStack;
ProfileStack.js
export const ProfileStack = ({ navigation }) => {
return (
<Stack.Navigator>
<Stack.Screen
name="Profile"
component={Profile}
/>
<Stack.Screen
name="EditProfile"
component={EditProfile}
/>
</Stack.Navigator>
);
};
getFieldValue function
export const getFieldValue = (collection, userid) => {
firestore()
.collection(collection)
.where("userid", "==", userid)
.get()
.then((querySnapshot) => {
if (querySnapshot.size === 0) {
return "";
}
if (querySnapshot.size === 1) {
const { name } = querySnapshot[0].data();
return name;
}
})
.catch((e) => console.log(e));
};
Routing file
const Routes = () => {
// Set an initializing state whilst Firebase connects
const [initializing, setInitializing] = React.useState(true);
const { user, setUser } = React.useContext(AuthContext);
// Handle user state changes
const onAuthStateChanged = (user) => {
setUser(user);
if (initializing) setInitializing(false);
};
React.useEffect(() => {
RNBootSplash.hide();
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
if (initializing) return null;
return (
<NavigationContainer>
{user ? <AppStack /> : <AuthStack />}
</NavigationContainer>
);
};
export default Routes;

How to use two contexts for authentication and subscription in Chrome extension?

I want to display the Stripe subscription form after user has signed up into the extension. I want to display home to a subscribed user. And whenever the user opens the extension the home should be displayed if he has already subscribed. If not, it should display subscription form.
But the problem is my app is displaying both the home and subscription form to a subscribed user.
Here is my private route code:
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const { currentUser, subscriptionStatus } = useContext(AuthContext);
return (
<Route
{...rest}
render={(routeProps) =>
!!currentUser ? (
!!subscriptionStatus ? (
<RouteComponent {...routeProps} />
)
: (
<Redirect to={"/subscribe"} />
)
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
This is my auth context provider:
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [pending, setPending] = useState(true);
const [emailVerified, setEmailVerified] = useState(true);
const [helper, setHelper] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(async(user) => {
setCurrentUser(user);
if(!user.emailVerified){
setEmailVerified(false);
}else{
setEmailVerified(true);
const fetchData = async () => {
const token = user && (await user.getIdToken());
const payloadHeader = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
};
const status = await fetch('http://localhost:3000/is-subscribed', payloadHeader).then(r => r.json());
if(status == 'active'){
setSubscriptionStatus(status);
setPending(false);
}else{
setPending(false);
}
}
fetchData();
}
});
}, []);
if (pending && helper) {
return <Loader />;
}
if(!emailVerified){
return <>Please verify your email</>
}
return (
<AuthContext.Provider
value={{
currentUser, subscriptionStatus
}}
>
{children}
</AuthContext.Provider>
);
};
Any idea on this?
The easiest option would be redirecting your customers to Checkout to pay, and handling the successful payments in your Firebase app via webhooks, but you can also use the custom flow if you prefer.

Why redux state is not render correctly?

Good day, I faced an issue when tried to push the user to the dashboard after the user login correctly but it didn't, here is the code below:
LoginForm.js
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);//values={email:'..', pass:'..'}
if (formValidation()) {
dispatch(NewUserLogin(values));
console.log(isAuth); //print false but in redux state print true
if (isAuth) history.push('/dashboard');
}
};
LoginAction.js
export const NewUserLogin = (formValues) => async (dispatch) => {
try {
dispatch(loginPending());
const { status, message } = await LoginAPIRequest(formValues);
if (status === 'success') {
dispatch(loginSuccess(message));
} else {
dispatch(loginFailure(message));
}
console.log(status);
console.log(message);
} catch (error) {
dispatch(loginFailure(error.message));
}
};
loginSlice.js
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
isLoading: false,
isAuth: false,
error: '',
};
const loginSlice = createSlice({
name: 'Login',
initialState,
reducers: {
loginPending: (state) => {
state.isLoading = true;
},
loginSuccess: (state, { payload }) => {
state.isLoading = false;
state.isAuth = true;
state.message = payload;
state.error = '';
},
loginFailure: (state, { payload }) => {
//actions.payload or shortcut {payload}
state.isLoading = false;
state.error = payload;
},
},
});
const { reducer, actions } = loginSlice;
export const { loginPending, loginSuccess, loginFailure } = actions;
export default reducer;
userAPI.js
import { createEndpointsAPI, ENDPOINTS } from './index';
export const LoginAPIRequest = (formValues) => {
return new Promise(async (resolve, reject) => {
//call api
try {
await createEndpointsAPI(ENDPOINTS.LOGIN)
.create(formValues)
.then((res) => {
resolve(res.data);
if (res.data.status === 'success') {
resolve(res.data);
sessionStorage.setItem('accessJWT', res.data.accessJWT);
localStorage.setItem('sms', JSON.stringify(res.data.refreshJWT));
}
console.log(res.data);
})
.catch((err) => {
reject(err);
});
} catch (error) {
console.log(error);
reject(error);
}
});
};
index.js (root API)
import axios from 'axios';
export const ENDPOINTS = {
LOGIN: 'user/login',
LOGOUT: 'user/logout',
REGISTER: 'user/register',
};
const baseURL = 'http://localhost:3040/v2/';
export const createEndpointsAPI = (endpoint) => {
let url = baseURL + endpoint + '/';
return {
fetchAll: () => axios.get(url),
fetchById: (id) => axios.get(url + id),
create: (newData) => axios.post(url, newData),
update: (updateData, id) => axios.put(url + id, updateData),
delete: (id) => axios.delete(url + id),
};
};
App.js
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Switch>
<Route path='/' exact>
<Login />
</Route>
<PrivateRoute path='/dashboard'>
<Dashboard />
</PrivateRoute>
<Route path='*' component={() => '404 NOT FOUND'} />
</Switch>
</Router>
</MuiThemeProvider>
PrivateRoute.js
import { useSelector } from 'react-redux';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuth } = useSelector((state) => state.login);
console.log(isAuth);
return (
<Route
{...rest}
render={(props) => {
isAuth ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
);
}}
/>
);
};
export default PrivateRoute;
The problem is, isAuth is a redux state, it should return true when the user login correctly, but it's not, I console.log(isAuth) and it prints false for the first time even user login correctly, and if I click login one more time it prints true in the console log and redirects the user to the dashboard page. I don't know why isAuth is returned false for the first time when use is login correctly? Please help check the above code from top to bottom, I provide you everythings.
The log: console.log(isAuth); logs a stale closure, you could try an effect on isAuth and redirect when it's true.
Here is an example:
const Component = (propps) => {
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
//...dispatches but doesn't check isAuth
};
useEffect(() => {
//go to dashboard if isAuth is true
if (isAuth) history.push('/dashboard');
}, [isAuth]);//run effect when isAuth changes
};

How do you wait for useAuth useEffect to return the current user state?

I have a bit of a problem implementing authentication for my React application. I followed this link to get the authentication going. Here's my App component:
function App() {
return (
<ProvideAuth>
<BrowserRouter>
<Header />
<Switch>
<PrivateRoute exact path="/">
<Dashboard />
</PrivateRoute>
<Route path="/login">
<Login />
</Route>
</Switch>
</BrowserRouter>
</ProvideAuth>
);
}
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
console.log("USER: ", auth.user);
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}
export default App;
Login component:
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let history = useHistory();
let location = useLocation();
let auth = useAuth();
let { from } = location.state || { from: { pathname: "/" } }
let login = (e) => {
auth.signin(email, password, () => {
history.replace(from);
});
};
return (
<div>
<input onChange={e => setEmail(e.target.value)} value={email} type="email" />
<input onChange={e => setPassword(e.target.value)} value={password} type="password" />
</div>
)
}
export default Login;
Finally use-auth.js:
const authContext = createContext();
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password, callback) => {
axios.post(`${apiUrl}/sign_in`, {
'email': email,
'password': password
},
{
headers: {
'Content-Type': 'application/json'
}
}).then(res => {
const expiryDate = new Date(new Date().getTime() + 6 * 60 * 60 * 1000).toUTCString();
document.cookie = `access-token=${res.headers['access-token']}; path=/; expires=${expiryDate}; secure; samesite=lax`;
return res.data
})
.then(data => {
setUser(data.data);
callback();
})
.catch(e => {
setUser(null);
});
};
const signout = () => {
document.cookie = "access-token=; expires = Thu, 01 Jan 1970 00:00:00 GMT";
setUser(null);
}
useEffect(() => {
const cookies = getCookies();
if (cookies['access-token']) {
axios.get(`${apiUrl}/user_info`, {
headers: {
...cookies
}
}).then(res => {
return res.data;
})
.then(data => {
setUser(data);
})
.catch(e => {
setUser(null);
})
} else {
setUser(null);
}
}, []);
return {
user,
signin,
signout
}
}
function getCookies() {
let cookies = document.cookie.split(';');
let authTokens = {
'access-token': null
};
for (const cookie of cookies) {
let cookiePair = cookie.split('=');
if (authTokens.hasOwnProperty(cookiePair[0].trim().toLowerCase()))
authTokens[cookiePair[0].trim()] = decodeURIComponent(cookiePair[1]);
}
return authTokens;
}
and then the dashboard component is the homepage. Nothing interesting.
The problem is when a user is in fact logged in (the access-token cookie is set as well as other tokens), they're still routed to the login page because of the fact that calling the API which checks that these tokens are valid is asynchronous, so the user is set to null initially.
What am I missing here? how can I wait until the API response is returned without blocking the user interface? Should I save user state in the redux state or is there some other work around?
Thanks a lot!
Like Jonas Wilms suggested, I added a loading state variable in user-auth similar to user and set it to true before each request and false after the request is completed.
In my App component, I changed the PrivateRoute function to show a loading spinner as long as the user state is loading. When it's set to false, I check whether the user is logged in or not and show the Dashboard component or redirect to login page accordingly.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.loading ?
<Loading /> :
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)} />
)
}

Categories

Resources