ccnat
ccnat

Reputation: 125

React useContext doesn't update the child component, but works on page refresh

I have a navbar component that has a submenu. Once logged in, the submenu of the navbar should change. I used the hook useContext, but it doesn't refresh the navbar component when the user logs. It works fine when I refresh the page. Where is my code problem?

APP COMPONENT

import React, { useState, useEffect } from "react";
import logo from "./assets/logoBusca.png";
import "./App.css";
import { Route } from "wouter";
import Home from "./components/Home";
import Login from "./components/Login";
import Post from "./components/Post";
import NavbarUser from "./components/NavbarUser";
import { AuthContext } from "./context/AuthContext";
import logic from "../src/logic";

function App() {
  const [user, setUser] = useState(false);

  useEffect(() => {
    (async () => {
      const loggedIn = await logic.isUserLoggedIn;
      if (loggedIn) setUser(true);
    })();
  }, [user]);

  return (
    <AuthContext.Provider value={user}>
      <div className="App">
        <header className="App-header">
          <div className="images">
            <div className="logo">
              <a href="/">
                <img src={logo} alt="logo" />
              </a>
            </div>
            <div className="user_flags">
              <NavbarUser />
            </div>
          </div>
        </header>
        <Route path="/">
          <Home />
        </Route>
        <Route path="/login">
          <Login />
        </Route>
        <Route path="/nuevabusqueda">
          <Post />
        </Route>
      </div>
    </AuthContext.Provider>
  );
}

export default App;

NAVBAR COMPONENT

import React, { useContext } from "react";
import userIcon from "../../assets/userIcon.png";
import { AuthContext } from "../../context/AuthContext";

export default function NavbarUser() {
  const isAuthenticated = useContext(AuthContext);

  return (
    <>
      {!isAuthenticated ? (
        <div className="navbar-item has-dropdown is-hoverable">
          <img src={userIcon} alt="user" />
          <div className="navbar-dropdown">
            <a href="/login" className="navbar-item" id="item_login">
              Login
            </a>
            <hr className="navbar-divider" />
            <a href="/registro" className="navbar-item" id="item_register">
              Registro
            </a>
          </div>
        </div>
      ) : (
        <div className="navbar-item has-dropdown is-hoverable">
          <img src={userIcon} alt="user" />
          <div className="navbar-dropdown">
            <a href="/datos" className="navbar-item" id="item_login">
              Perfil
            </a>
            <hr className="navbar-divider" />
            <a href="/user" className="navbar-item" id="item_register">
              Logout
            </a>
          </div>
        </div>
      )}
    </>
  );
}

CONTEXT COMPONENT

import { createContext } from "react";

export const AuthContext = createContext();

LOGIC COMPONENT

import buscasosApi from "../data";

const logic = {
  set userToken(token) {
    sessionStorage.userToken = token;
  },

  get userToken() {
    if (sessionStorage.userToken === null) return null;
    if (sessionStorage.userToken === undefined) return undefined;
    return sessionStorage.userToken;
  },

  get isUserLoggedIn() {
    return this.userToken;
  },

  loginUser(email, password) {
    return (async () => {
      try {
        const { token } = await buscasosApi.authenticateUser(email, password);
        this.userToken = token;
      } catch (error) {
        throw new Error(error.message);
      }
    })();
  },
};
export default logic;

Upvotes: 1

Views: 1681

Answers (2)

Mr. Hedgehog
Mr. Hedgehog

Reputation: 2885

It seems to me that you are not updating state. Initially your state is false. After first render, your effect is fired and state is again set to false. After that your effect don't run anymore, since it depends on state that it should change. No state change - no effect - no state change again. Also, if state will change, it will trigger effect, but you will have no need for this.

Your goal here is to build a system that will:

  1. Check current status on load
  2. Update (or recheck) status when changed

To do this, you need some way to asynchronously send messages from logic to react. You can do this with some kind of subscription, like this:

const logic = {
  set userToken(token) {
    sessionStorage.userToken = token;
  },

  get userToken() {
    if (sessionStorage.userToken === null) return null;
    if (sessionStorage.userToken === undefined) return undefined;
    return sessionStorage.userToken;
  },

  get isUserLoggedIn() {
    return this.userToken;
  },

  loginUser(email, password) {
    return (async () => {
      try {
        const { token } = await buscasosApi.authenticateUser(email, password);
        this.userToken = token;
        this.notify(true);
      } catch (error) {
        throw new Error(error.message);
      }
    })();
  },
  subscribers: new Set(),
  subscribe(fn) {
    this.subscribers.add(fn);
    return () => {
      this.subscribers.remove(fn);
    };
  },
  notify(status) {
    this.subscribers.forEach((fn) => fn(status));
  },
};

function useAuthStatus() {
  let [state, setState] = useState("checking");
  let setStatus = useCallback(
    (status) => setState(status ? "authenticated" : "not_authenticated"),
    [setState]
  );
  useEffect(function () {
    return logic.subscribe(setStatus);
  }, []);

  useEffect(function () {
    setStatus(logic.isUserLoggedIn);
  }, []);

  return state;
}

Notice, that now there is three possible states - 'checking', 'authenticated' and 'not_authenticated'. It is more detailed and will prevent some errors. For example if you would want to redirect user to login page when they are not authenticated.

Upvotes: 1

J&#243;zef Podlecki
J&#243;zef Podlecki

Reputation: 11305

I think Login component doesn't call setUser(true)

Here's an example how it might work.

const { useState, useEffect, createContext, useContext } = React;

const AuthContext = createContext();

const Login = () => {
  const [isAuthenticated, setAuth] = useContext(AuthContext);
  const onClick = () => setAuth(true);
  return <button disabled={isAuthenticated} onClick={onClick}>Login</button>
}

const App = () => {
  const [isAuthenticated] = useContext(AuthContext);
  
  return <div>
    <Login/>
    <button disabled={!isAuthenticated}>{isAuthenticated ? "Authenticated" : "Not Authenticated"}</button>
  </div>;
}

const AuthProvider = ({children}) => {
  const [isAuthenticated, setAuth] = useState(false);
  
  return <AuthContext.Provider value={[isAuthenticated, setAuth]}>
    {children}
  </AuthContext.Provider>;
}

ReactDOM.render(
    <AuthProvider>
      <App />
    </AuthProvider>,
    document.getElementById('root')
  );
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>

Upvotes: 1

Related Questions