React useContext is gives output after delay

Viewed 1234

I need to use the data coming from useContext to authenticate if the user logged in is Admin or not. But it gives the value "undefined" first and then after a delay of 5 seconds it gives the actual value. But by then, the code breaks because it cannot work with "undefined".

import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';

const Admin = () => {
    const history = useHistory();
    const { userData } = useContext(AuthContext);
    console.log(userData); // ****** This first gives {token: undefined, user: undefined}
    /***************************** Then it gives the actual data after 5-10 seconds delay
     * {token: "eyJhbGciOiJIUzI", user: {…}}
        token: "eyJhbGciOiJIUzI"
        user:
        isAdmin: true
        __v: 0
        _id: "5eea03a736b70722c83a7b63"
    *******************************************************************/
    // I WANT TO FETCH "isAdmin" // so that I can work on further code
    if (!userData.user.isAdmin) {
        history.push("/");
    };
    return <h1>Admin Panel</h1>

};

Do I need to use async/await? Because I tried doing it in a custom async funtion and it was throwing another error that we cannot use useContext in non-react-function.

3 Answers

I took ideas from the comments and found a work around. This works fine as well :

import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';
import AdminPanel from './AdminPanel';

const Admin = () => {
    const token = localStorage.getItem('auth-token');
    const history = useHistory();
    const { userData } = useContext(AuthContext);

    //send item to backend
    const saveNewItem = async (data) => {
        const url = "http://localhost:5000/api/items";
        try {
            const res = await axios.post(url, data, {
                headers: { "x-auth-token": token }
            });
            console.log(res.data);    
        } catch (error) {
            console.log(error.response);
        }
    };

    /********************************
     * Handling Authentication logic
     ********************************/
    };
    if (!token) {
        history.push("/");
    };
    /********************************
     * Handling Authentication logic
     ********************************/

    return <div>
        {
            userData.user !== undefined && userData.user.isAdmin 
            ? <AdminPanel saveNewItem={saveNewItem}/>
            :
            <div className="container p-5 text-center">
            <div class="spinner-border text-light" role="status">
                <span class="sr-only">Loading...</span>
            </div>
            </div>   
        }
        {/* <AdminPanel saveNewItem={saveNewItem}/> */}
    </div>

};
export default Admin;

I hope this makes sense as it works for me. (Let me know if there's any issue with the security in this)

You can setup a useEffect hook to perform a side effect(redirect to "/") when state(userData) changes:

Replace:

if (!userData.user.isAdmin) {
  history.push("/");
}

with:

useEffect(() => {
  if (userData && userData.user && !userData.user.isAdmin) {
    history.push("/");
  }
}, [userData]); // tell React to skip applying this effect if userData hasn't changed between re-renders. 

More about Using the Effect Hook

Then, you can return different components based on different situations:

if (!userData) return <p>Loading</p>;
if (!userData.user.isAdmin) return <p>You are not an admin. Redirecting...</p>;
return <h1>Admin Panel</h1>;

You might want to consider extending the Route component and adding a loading state on your AuthContext (and maybe error) while it's authenticating.

AdminOnlyRoute.js

import React from "react";
import { Redirect, useLocation, Route } from "react-router-dom";
import { AuthContext } from '../../context/AuthContext';

const AdminOnlyRoute = (props) => {
  // give the context a loading and or an error state
  const { userData, loading, error } = useContext(AuthContext);

  if (loading) {
    // Some loader maybe?
    return <div>Authenticating...</div>
  }

  if (error) {
    return <div>Authentication error.</div>
  }

  if (userData && !userData.user.isAdmin)
    return (
      <Redirect to="/" />
    );

  return <Route {...props} />;
};

export default AdminOnlyRoute;

You can use this component as such:

<Router>
  <Switch>
    <AdminOnlyRoute path="/admin" component={<div>Admin</div>} />
    <Route path="/" component={<div>Home</div>} />
  </Switch>
</Router>

Update: There's a very helpful example from react-router website with this workflow https://reacttraining.com/react-router/web/example/auth-workflow

Related