useContext call preventing subsequent useState update - reactjs

I have a provider wrapper around some routes
<Provider>
<Route path={ROUTES.SIGNING}><SignIn /></Route>
<PrivateRoute path={ROUTES.PRIVATE}><Private /></PrivateRoute>
</Provider>
The Provider is simply a wrapper for a userContext
import React, { useState } from 'react';
import UserContext from '../../user.context';
let defaultUser = '';
try {
defaultUser = JSON.parse(localStorage.getItem('profile'));
} catch {
defaultUser = '';
}
function Provider(props) {
const [user, setUser] = useState(defaultUser);
return <UserContext.Provider value={{ user, setUser }}>{props.children}</UserContext.Provider>
}
export default Provider;
My <SignIn /> Component waits for a response from a data service then 1. attempts to update the setter from userContext and then trys to update it's own useState function. It never seems to execute the internal useState function.
function SignIn() {
const { user, setUser } = useContext(UserContext);
const [formStatus, setFormStatus] = useState();
async function handleSubmit(e) {
e.preventDefault();
const result = await signin(credentials);
setUser({ isInternal: result.isInternal, clientId: result.clientId });
setFormStatus((curStatus) => ({ ...curStatus, state: FORMSTATUS.COMPLETED }));
}
Why would the setStatus never seem to fire? I think it's because setUser updates the Provider which is a higher level component than the child SignIn Page. Any help would be great

You have a race condition. To resolve, you need to specify that your local state update should complete before the context state update.
Solution:
Use React's functional form to set local state.
Then call your context state update within the functional set state call.
This way, you know the local state has completed before you update context.

Related

React Context API current

Okay...what is happening here cause I don't undrestand? I have an react context api where I store the current user data. In the login function I console log the currentUser and everything looks fine, but then when I write the currentUser in chrome dev tools, it appears undefined. In localStorage I also have the data. What is happening? What am I doing wrong? Can someone, please help me.
Here is my code:
authContext.js
import { createContext, useState, useEffect } from "react";
import axios from "axios";
export const AuthContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(
JSON.parse(localStorage.getItem("user")) || null
);
const login = async (inputs) => {
try {
const res = await axios.post("/login", inputs);
setCurrentUser(res.data);
console.log("res.data: ", res.data); //returns data
console.log("currentUser ", currentUser); //returns data
} catch (error) {
console.log(error);
}
};
const logout = async () => {
localStorage.clear();
setCurrentUser(null);
};
useEffect(() => {
localStorage.setItem("user", JSON.stringify(currentUser));
}, [currentUser]);
return (
<AuthContext.Provider value={{ currentUser, login, logout }}>
{children}
</AuthContext.Provider>
);
};
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { AuthContextProvider } from "./ccontext/authContext";
import App from "./App";
import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);
Login.jsx
/*...*/
const LoginForm = () => {
const [error, setError] = useState(null);
const navigate = useNavigate();
const { login } = useContext(AuthContext);
const handleFormSubmit = (values, actions) => {
try {
login(values);
actions.resetForm();
navigate("/");
console.log("logged in");
} catch (err) {
setError(err.response.data);
}
};
/*...*/
Updating state is best seen like an asynchronous operation. You cannot set state in a function / effect and expect it to immediately be updated, on the spot. Well, it technically is, but you won't see the updates in your "already-running" function.
I am pretty sure that if you extract your log in the component root it will display the appropriate value after the login function finishes executing and the component properly sets the new state.
If you do need the value in the function you should directly use res.data.
A deeper dive:
Whenever your login function runs it forms a closure around your current values (including currentUser which is undefined at the moment).
When you update the currentUser in the login function you basically inform react that you need to update that value. It will handle this in the background, preparing the state for the next render, but your login function will keep running with whatever values it started with. Your "new" state values will not be available until you run the function again. This is because the already-running function "closed over" old values, so it can only reference those.
As a side note, if you use a ref for instance you would not have this problem. Why? Because refs do not participate in the react lifecycle. When you modify a ref it changes on the spot. You will have the updated value precisely on the next line. State does not work like that, it is coupled to the component lifecycle, so it will update on the next render.

React Context value never updates

I have the following code as a component that returns a context. For some reason when I call the updateUser function it is not setting the state, I keep getting a blank object. Is there something different I have to do to a function like that when it has parameters?
import React, { useState } from "react";
const UserContext = React.createContext({
user: {},
updateUser: (incomingUser) => {},
});
const UserData = (props) => {
const [user, setUser] = useState({});
const updateUser = (incomingUser) => {
setUser(incomingUser);
console.log(`user=${JSON.stringify(incomingUser)}`);
};
return (
<UserContext.Provider value={{ user, updateUser }}>
{props.children}
</UserContext.Provider>
);
};
export { UserData, UserContext };
Get rid of the default value for UserContext, or initialise it as null:
const UserContext = React.createContext(null);
Per the React Docs:
When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree.
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This default value can be helpful for testing components in isolation without wrapping them.

How to wait for the render to complete to get useContext when using useEffect?

I have a conditional redirect from UserPage as shown below to an external authentication service. The condition is in such a way that if my UserContext does not contain the user object then the page will be redirected to https://urltoauthenticate:5000/user for user authentication. The condition is satisfied in the first run and goes to the authentication service
But the issue is happening after successful authentication. In the case of successful authentication even though the UserContext provides the user object, it will be null in the first render. Here how can I wait till the user object is available and ready so that the page will not be redirected to the authentication service again?
import React, { useEffect, useContext } from 'react';
import { UserContext } from '../context/UserContext';
export default function UserPage () {
const user = useContext(UserContext)
useEffect(() => {
if(user === null{
window.location.assign("https://urltoauthenticate:5000/user");
}
}, [users]);
return(
<div>Hello {user.name}</div>
)
}
Instead of waiting for your context to be updated, make a redirect to the user's page when it actually gets updated. To make it you can put on your login page a component like this:
export const RedirectIfLoggedIn: React.FC = () => {
const user = React.useContext(UserContext);
return user ? <Redirect to={userPath} /> : null;
};
Additionally, pass that redirect guard to your routing:
export const PrivateRoute: React.FC<{
component: React.FC;
path: string;
exact: boolean;
}> = (props) => {
const user = useContext(UserContext);
return user ? (
<Route path={props.path} exact={props.exact} component={props.component} />
) : (
<Redirect to={loginPath} />
);
};
export const Routes: React.FC = () => {
return (
<Switch>
<Route path={loginPath} component={LoginPage} />
<PrivateRoute path={userPath} component={UserPage} />
...
</Switch>
);
};
Just simply add useState
import React, { useEffect, useContext, useState } from 'react';
import { UserContext } from '../context/UserContext';
export default function UserPage () {
const [render, setRender] = useState(false)
const user = useContext(UserContext)
useEffect(() => {
setRender(true)
}, []);
useEffect(() => {
if(render === true) {
if(user === null{
window.location.assign("https://urltoauthenticate:5000/user");
}
}
}, [render, user]);
return(
<div>Hello {user.name}</div>
)
}
I suspect there are couple of issues in your code.
users as dependency is passed while you have user. Note plural and singular use.
user should never be null. This returns context object. So, unless you destructed const {user} this will not be null.
Unless you pass the null value, it won't be null. null !== undefined
So your hook makes no sense to me. It never runs. Your resurrection is happening from somewhere else.
Now, to the point how to wait for the value?
No, you don't need to wait for the value. The context value must be consumed after you have defined it on the provider.
I guess you have not defined the default value while creating the context.
const {user} = useContext(UserContext)
The user is undefined. There's nothing wrong here. You're just consuming value before you provide in the provider.
What should be done?
When you login successfully, provide the value as user in the provider and then you can consume the value. Also, you don't need to use the effect hook. You will be able to get the value just so. I mean you can return redirect just after the user value const definition.
Make sure the provider is somewhere as parent component of UserPage component.

Share API data with other components by using React Hook, NO REDUX

I'm experimenting with Hooks without any state management tool (such as Redux), to get the same kind of behavior/structure I could have by using a traditional structure of classes + redux.
Usually, with a class base code I would:
ComponentDidMount dispatch to Call the API
Use actions and reducers to store the data in Redux
Share the data to any component I want by using mapStateToProps
And here where the problem is by using Hooks without Redux: 'Share the DATA with any component'.
The following example is the way I have found to share states between components by Hooks:
//app.js
import React, { useReducer } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Routes from '../../routes'
import Header from '../Shared/Header'
import Footer from '../Shared/Footer'
export const AppContext = React.createContext();
// Set up Initial State
const initialState = {
userType: '',
};
function reducer(state, action) {
switch (action.type) {
case 'USER_PROFILE_TYPE':
return {
userType: action.data === 'Student' ? true : false
};
default:
return initialState;
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} />
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
)
}
export default App
// profile.js
import React, { useEffect, useState, useContext} from 'react'
import { URLS } from '../../../constants'
import ProfileDeleteButton from './ProfileDeleteButton'
import DialogDelete from './DialogDelete'
import api from '../../../helpers/API';
// Import Context
import { AppContext } from '../../Core'
const Profile = props => {
// Share userType State
const {state, dispatch} = useContext(AppContext);
const userType = type => {
dispatch({ type: 'USER_PROFILE_TYPE', data: type }); <--- Here the action to call the reducer in the App.js file
};
// Profile API call
const [ profileData, setProfileData ] = useState({});
useEffect(() => {
fetchUserProfile()
}, [])
const fetchUserProfile = async () => {
try {
const data = await api
.get(URLS.PROFILE);
const userAttributes = data.data.data.attributes;
userType(userAttributes.type) <--- here I am passing the api response
}
catch ({ response }) {
console.log('THIS IS THE RESPONSE ==> ', response.data.errors);
}
}
etc.... not important what's happening after this...
now, the only way for me to see the value of userType is to pass it as a prop to the <Header /> component.
// app.js
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} /> <--passing here the userType as prop
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
Let's say that I want to pass that userType value to children of <Routes />.
Here an example:
<AppContext.Provider value={{ state, dispatch }}>
<Routes userType={state.userType} />
</AppContext.Provider>
and then, inside <Routes /> ...
const Routes = () =>
<Switch>
<PrivateRoute exact path="/courses" component={Courses} userType={state.userType} />
</Switch>
I don't like it. It's not clean, sustainable or scalable.
Any suggestions on how to make the codebase better?
Many thanks
Joe
You don't need to pass the state as a prop to every component. Using context you can gain access to state properity in your reducer inside every child component of parent Provider. Like you have already done in the Profile.js
const {state, dispatch} = useContext(AppContext);
State property here contains state property in the reducer. So you can gain access to it by state.userType
Everything you need is within your context.
The only changes I would make is spread the data instead of trying to access it one at a time something like this
<AppContext.Provider value={{ ....state, dispatch }}>
then use const context = useContext(AppContext) within the component you need to access the data.
context.userType

How would I use React Hooks to replace my withAuth() HOC?

I've been spending a bunch of time reading up on React Hooks, and while the functionality seems more intuitive, readable, and concise than using classes with local state and lifecycle methods, I keep reading references to Hooks being a replacement for HOCs.
The primary HOC I have used in React apps is withAuth -- basically a function that checks to see if the currentUser (stored in Redux state) is authenticated, and if so, to render the wrapped component.
Here is an implementation of this:
import React, { Component } from "react";
import { connect } from "react-redux";
export default function withAuth(ComponentToBeRendered) {
class Authenticate extends Component {
componentWillMount() {
if (this.props.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
componentWillUpdate(nextProps) {
if (nextProps.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
render() {
return <ComponentToBeRendered {...this.props} />;
}
}
function mapStateToProps(state) {
return { isAuthenticated: state.currentUser.isAuthenticated };
}
return connect(mapStateToProps)(Authenticate);
}
What I can't see is how I can replace this HOC with hooks, especially since hooks don't run until after the render method is called. That means I would not be able to use a hook on what would have formerly been ProtectedComponent (wrapped with withAuth) to determine whether to render it or not since it would already be rendered.
What is the new fancy hook way to handle this type of scenario?
render()
We can reframe the question of 'to render or not to render' a tiny bit. The render method will always be called before either hook-based callbacks or lifecycle methods. This holds except for some soon-to-be deprecated lifecycle methods.
So instead, your render method (or functional component) has to handle all its possible states, including states that require nothing be rendered. Either that, or the job of rendering nothing can be lifted up to a parent component. It's the difference between:
const Child = (props) => props.yes && <div>Hi</div>;
// ...
<Parent>
<Child yes={props.childYes} />
</Parent>
and
const Child = (props) => <div>Hi</div>;
// ...
<Parent>
{props.childYes && <Child />}
</Parent>
Deciding which one of these to use is situational.
Hooks
There are ways of using hooks to solve the same problems the HOCs do. I'd start with what the HOC offers; a way of accessing user data on the application state, and redirecting to /signin when the data signifies an invalid session. We can provide both of those things with hooks.
import { useSelector } from "react-redux";
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const MySecurePage = props => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated && <MyPage {...props} />;
};
A couple of things happening in the example above. We're using the useSelector hook from react-redux to access the the state just as we were previously doing using connect, only with much less code.
We're also using the value we get from useSelector to conditionally fire a side effect with the useEffect hook. By default the callback we pass to useEffect is called after each render. But here we also pass an array of the dependencies, which tells React we only want the effect to fire when a dependency changes (in addition to the first render, which always fires the effect). Thus we will be redirected when isAuthenticated starts out false, or becomes false.
While this example used a component definition, this works as a custom hook as well:
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const useAuth = () => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated;
};
const MySecurePage = (props) => {
return useAuth() && <MyPage {...props} />;
};
One last thing - you might wonder about doing something like this:
const AuthWrapper = (props) => useAuth() && props.children;
in order to be able to do things like this:
<AuthWrapper>
<Sensitive />
<View />
<Elements />
</AuthWrapper>
You may well decide this last example is the approach for you, but I would read this before deciding.
Building on the answer provided by backtick, this chunk of code should do what you're looking for:
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
const withAuth = (ComponentToBeRendered) => {
const mapState = (state) => ({
isAuthenticated: state.currentUser.isAuthenticated,
});
const Authenticate = (props) => {
const { isAuthenticated } = useSelector(mapState);
useEffect(() => {
if (!isAuthenticated) {
props.history.push("/signin");
}
}, [isAuthenticated]);
return isAuthenticated && <ComponentToBeRendered {...props} />;
};
return Authenticate;
};
export default withAuth;
You could render this in a container using React-Router-DOM as such:
import withAuth from "../hocs/withAuth"
import Component from "../components/Component"
// ...
<Route path='...' component={withAuth(Component)} />

Resources