Search code examples
reactjsreact-routerreact-bootstrapreact-context

React Context Re-rendering Issue when Using Context for User Login


I am encountering an issue with React context while implementing user login functionality on my website. Users can log in as dummy users, and I've implemented this using React contexts. However, after a user logs in and navigates away from the page, the context doesn't persist, leading me to believe that the component is re-rendering.

Gif of issue

GitHub: GitHub Repository

Additionally, you can experience the issue on the live website: Live Website

I would appreciate any assistance in understanding why the context is not persisting after navigating away from the page. Thank you for your help.

import { createContext, useState } from "react";

export const UserContext = createContext();

export const UserProvider = (props) => {
  const [user, setUser] = useState("grumpy19");
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {props.children}
    </UserContext.Provider>
  );
};
import { useEffect, useState } from "react";
import { getUsers } from "../../utils/utils";
import { UserContext } from "../../contexts/UserContent";
import { useContext } from "react";
import "./Users.css";
import Spinner from "react-bootstrap/Spinner";
import { Card, CardGroup } from "react-bootstrap";
import { Badge } from "react-bootstrap";
import Stack from "react-bootstrap/Stack";

export default function Users() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [err, setErr] = useState(null);
  const { setUser, user } = useContext(UserContext);

  function handleAvatarClick(username) {
    setUser(username);
  }

  useEffect(() => {
    setIsLoading(true);
    setErr(false);
    getUsers()
      .then(({ users }) => {
        setUsers(users);
        setIsLoading(false);
      })
      .catch((err) => {
        setIsLoading(false);
        setErr(err);
      });
  }, []);

  if (err)
    return (
      <div>
        <h1>Whoops</h1>
        <h2>Status: {err.status}</h2>
        <h2>{err.msg}</h2>
      </div>
    );

  return (
    <div className="user-container">
      <h2 id="user-heading">Users</h2>
      <p id="user-desc">Click on a users image to log in</p>
      <p id="current-user">
        Hello <Badge bg="dark">{user}</Badge> !
      </p>

      {isLoading ? (
        <Spinner animation="border" variant="primary" className="spinner" />
      ) : null}

      <CardGroup id="users-list">
        <ul>
          {users.map((person, index) => {
            return (
              <Card
                key={index}
                className="user-card"
                onClick={() => {
                  handleAvatarClick(person.username);
                }}
              >
                <Stack className="user-info" direction="horizontal" gap={3}>
                  <div className="p-2">{person.name}</div>
                  <div className="p-2 ms-auto">
                    {" "}
                    <Badge>{person.username}</Badge>
                  </div>
                </Stack>

                <Card.Img src={person.avatar_url} />
              </Card>
            );
          })}
        </ul>
      </CardGroup>
    </div>
  );
}

I have implemented similar code in a previous project, and it worked without any problems.

import { createContext, useState } from "react";

export const UserContext = createContext();

export const UserProvider = (props) => {
  const [user, setUser] = useState("Paul-R");
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {props.children}
    </UserContext.Provider>
  );
};
import { useEffect, useState } from "react";
import "./Users.css";
import { getUsers } from "../../utils.js/api";
import { UserContext } from "../../contexts/UserContent";
import { useContext } from "react";

export default function Users() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [err, setErr] = useState(null);
  const { setUser } = useContext(UserContext);

  function handleAvatarClick(username) {
    setUser(username);
  }

  useEffect(() => {
    setIsLoading(true);
    setErr(false);
    getUsers()
      .then(({ users }) => {
        setUsers(users);
        setIsLoading(false);
      })
      .catch((err) => {
        setIsLoading(false);
        setErr(err);
      });
  }, []);

  if (isLoading) return <p>Loading...</p>;

  if (err)
    return (
      <div>
        <h1>Whoops</h1>
        <h2>Status: {err.status}</h2>
        <h2>{err.msg}</h2>
      </div>
    );

  return (
    <div>
      <h1>Users</h1>
      <p>Click on a users image to log in</p>
      <ul>
        {users.map((user) => {
          return (
            <li key={user.username}>
              <p>username: {user.username}</p>
              <p>kudos: {user.kudos}</p>
              <img
                src={user.avatar_url}
                alt=""
                width="100"
                onClick={() => {
                  handleAvatarClick(user.username);
                }}
              />
            </li>
          );
        })}
      </ul>
    </div>
  );
}

Solution

  • The problem is that react-bootstrap's <Nav.Link> component does not know you're using react-router and so doesn't integrate with it. When you click the link, it is doing a page refresh, which resets all states in the app.

    You can use react-router's Link component as a standalone, like this:

    import { Link } from 'react-router';
    // ...
    <Link to="/">Home</Link>
    

    Or if you want to continue using react-bootstrap's Nav.Link, the as prop can be used to tell it to render using a different component:

    <Nav.Link as={Link} to="/">Home</Nav.Link>
    

    Or, one more option, you could write an onClick handler and have it call the function from useNavigate:

    import { useNavigate } from 'react-router';
    
    export default function Navigation() {
      const navigate = useNavigate();
      // ...
      <Nav.Link onClick={() => navigate("/")}>Home</Nav.Link>
    }