2

I have a UserContext that is set when App renders. App retrieves the current user from a server, and then sets the user context in a provider.

So whenever I navigating to a link, App renders, get's the current user from the server, and sets it. This allows all the children have access to the user.

Problem with <Redirect>

But I'm running into a problem when I use <Redirect>.

If the user updates on the server, then App needs to re-render in order to get the updated user object.

But on a redirect App doesn't re-render which leads to an outdated user context until the user refreshes the page in order to re-render App.

Example: Login to see your profile

In my code below I have a login button. When the user logs in the page redirects to their profile.

But even though the user is successfully logged in on the server, the user context hasn't updated. This is because redirect doesn't re-render App.

Is there a way to get redirect to re-render app or some other solution?

Code

The relevant code is below.

The full code is available on the repo here. Download, run npm i, npm start, and then either select and play Compounded Server/React in the debugger or run node currentUserServer/server.js to start the server without the debugger tools.

Frontend

App.js

import React, { useEffect, useContext, useState } from "react";
import { UserContext } from "./contexts/UserContext";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";

import Login from "./Login";
import Profile from "./Profile";

const currentUser = async () => {
  const user = await fetch("/users/current", {}).then(async (res) => {
    const userJson = await res.json();
    return userJson;
  });
  return user;
};
export default function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    currentUser().then((user) => {
      setUser(user);
    });
  }, []);

  return (
    <Router>
      <div className="App">
        <UserContext.Provider value={user}>
          <Switch>
            <Route path="/profile">
              <Profile />
            </Route>
            <Route path="/">
              <Login />
            </Route>
          </Switch>
        </UserContext.Provider>
      </div>
    </Router>
  );
}

Login.js

import React, { useContext, useState } from "react";
import { Redirect } from "react-router-dom";
import { UserContext } from "./contexts/UserContext";

export default function Login() {
  const [next, setNext] = useState(false);

  const currentUser = useContext(UserContext);

  return (
    <div>
      Logged In:{" "}
      {!currentUser || currentUser.message === "not logged in"
        ? "No One"
        : currentUser.username}{" "}
      <br></br>
      <button
        onClick={() => {
          fetch("/login", { method: "POST" }).then((res) => {
            if (res.status === 201) setNext(true);
          });
        }}
      >
        Login
      </button>
      <button
        onClick={() => {
          fetch("/logout", { method: "DELETE" });
        }}
      >
        Logout
      </button>
      {next && <Redirect to="/profile" />}
    </div>
  );
}

Profile.js

 import React, { useContext } from "react";
import { UserContext } from "./contexts/UserContext";

export default function Profile() {
  const currentUser = useContext(UserContext);

  return (
    <div>
      {currentUser && !currentUser.message
        ? "You're logged in and can edit your profile."
        : "You're not logged in."}
    </div>
  );
}
Dashiell Rose Bark-Huss
  • 2,173
  • 3
  • 28
  • 48

1 Answers1

0

I found two solutions solution.

#1 Quick fix but not as good: setRefresh

This works but seems to nullify the whole point of using context because we have to pass the state down.

I created a refresh state on App.js that triggers useEffect.

  useEffect(() => {
    if (refresh) {
      currentUser().then((user) => {
        setUser(user);
        setRefresh(false);
      });
    }
  }, [refresh]);

Now I can pass setRefresh as a prop to Login in order trigger useEffect to run again.

Full code here

#2 Better! setUser and getUser in context

This is sort of like the solution above but makes it so you're still taking advantage of context. You put setUser and getUser method on the context- inspired by this answer.

In app.js we create a user state. We set the value of UserContext to an object. The object contains user and setUser. And we also pass in getUser, a function that request the current user from the server.

export default function App() {
  const [user, setUser] = useState(null);
  const getUser = async () => {
    const user = await fetch("/users/current", {}).then(async (res) => {
      const userJson = await res.json();
      return userJson;
    });
    return user;
  };
  const value = { user, setUser, getUser };

Here the object is passed to the provider.

        <UserContext.Provider value={value}>

Now we have access to user, setUser, getUser anywhere there is a UserContext consumer.

const { user, setUser, getUser } = useContext(UserContext);

We can use getUser to ping the server for the current user. Then set that result as the user.

setUser(await getUser());
export default function Login(props) {
  const [next, setNext] = useState(false);

  const { user, setUser, getUser } = useContext(UserContext);

  return (
    <div>
      Logged In:{" "}
      {!user || user.message === "not logged in" ? "No One" : user.username}{" "}
      <br></br>
      <button
        onClick={() => {
          fetch("/login", { method: "POST" }).then(async (res) => {
            if (res.status === 201) {
              setUser(await getUser());
              setNext(true);
            }
          });
        }}
      >
        Login
      </button>

Full Code here

Dashiell Rose Bark-Huss
  • 2,173
  • 3
  • 28
  • 48