State from redux store gets lost/ reset when changing route

2.1k Views Asked by At

I have a simple app that so far only has a main login component, and a home page with a navbar which the user is supposed to be routed to once they login to the app. The user experience should be as follows: user selects name from blue dropdown menu, once that name is selected, they click the green login button which dispatches setLoggedInUser from loggedInSlice.js and sets the selected user as the authorize object For some reason , my state from my redux store is lost when I wrap my login button in a router link tag which leads to the home page , but it is not lost when that login button doesnt link anywhere. I want to understand why I am not retaining my redux authUser state once I navigate from the userLogin.js component to the Main.js Component.

Components:

App.js

import logo from "./logo.svg";
import "./App.css";
import TestComponent from "./components/TestComponent";
import Main from "./components/Main";
import { Routes, Route, Link } from "react-router-dom";
import { useState } from "react";
import { Card, Button, Accordion, Dropdown } from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.min.css";
import UserLogin from "./components/UserLogin";
import Nav from "./components/Nav";

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="home" element={<Main />} />
        <Route path="/" element={<UserLogin />} />
        <Route path="testcomponent" element={<TestComponent />} />
      </Routes>
    </div>
  );
}

export default App;

UserLogin.js

import React from "react";
import { Routes, Route, Link, useNavigate } from "react-router-dom";
import { Redirect } from "react-router-dom";
import { useState, useEffect } from "react";
import { Card, Button, Accordion, Dropdown } from "react-bootstrap";
import { useSelector, useDispatch } from "react-redux";
import "bootstrap/dist/css/bootstrap.min.css";
import { setLoggedInUser } from "../features/loggedInSlice";

export default function UserLogin() {
  let navigate = useNavigate();
  const users = useSelector((state) => state.users);
  const state = useSelector((state) => state);
  const dispatch = useDispatch();
  const [user, setUser] = useState("Select A User");
  const [authorizedUser, setauthorizedUser] = useState({});
  const handleSelect = (e) => {
    setUser(e.target.text);
  };

  function authorizeUser() {
    setauthorizedUser(
      users.filter((userName) => userName.firstName === user)[0]
    );
  }

  useEffect(() => {
    dispatch(setLoggedInUser(authorizedUser));
  }, [authorizedUser]);

  function authorizeLogin() {
    console.log(state);
  }

  console.log(state);

  return (
    <div>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        <Card style={{ width: "50%" }}>
          <Card.Header>Would you Rather?</Card.Header>
          <Card.Body>
            <Card.Title>Special title treatment</Card.Title>
            <Card.Text>
              With supporting text below as a natural lead-in to additional
              content.
            </Card.Text>
          </Card.Body>
          <Dropdown className="d-inline mx-2" onClick={handleSelect}>
            <Dropdown.Toggle
              id="dropdown-autoclose-true"
              style={{ width: "60%" }}
            >
              {user}
            </Dropdown.Toggle>

            <Dropdown.Menu style={{ width: "60%" }}>
              {users.map((user, index) => (
                <Dropdown.Item href="#" key={index}>
                  {user.firstName}
                </Dropdown.Item>
              ))}
            </Dropdown.Menu>
          </Dropdown>
          <div>
<Link to="home">
            <Button
              variant="success"
              style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
              onClick={authorizeUser}
            >
              Login
            </Button>
</Link>
          </div>
        </Card>

        <button onClick={authorizeLogin}>click</button>
      </div>
    </div>
  );
}

Main.js

import React from "react";
import NavBar from "./Nav";
import { Routes, Route, Link } from "react-router-dom";

export default function Main() {
  return (
    <div>
      <NavBar />
    </div>
  );
}

Nav.js

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { Card, Button, Accordion, Dropdown, Nav } from "react-bootstrap";

export default function NavBar() {
  const state = useSelector((state) => state);

  console.log(state);

  const loggedInFirstName = state.loggedIn.authUser.firstName;

  return (
    <div style={{ display: "flex" }}>
      <Nav justify variant="tabs" defaultActiveKey="/home">
        <Nav.Item>
          <Nav.Link>Home</Nav.Link>
        </Nav.Item>
        <Nav.Item>
          <Nav.Link>New Poll </Nav.Link>
        </Nav.Item>
        <Nav.Item>
          <Nav.Link>Leaderboard</Nav.Link>
        </Nav.Item>
      </Nav>
      <h5 style={{ paddingLeft: "15px", paddingTop: "15px" }}>Welcome</h5>
    </div>
  );
}

Redux Store and Slices:

store.js

import { configureStore } from "@reduxjs/toolkit";
import usersReducer from "../features/usersSlice";
import loggedIn from "../features/loggedInSlice";

export const store = configureStore({
  reducer: {
    users: usersReducer,
    loggedIn: loggedIn,
  },
});

loggedInSlice.js

import React from "react";

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  authUser: [],
};

export const loggedIn = createSlice({
  name: "loggedIn",
  initialState,
  reducers: {
    setLoggedInUser: (state, action) => {
      console.log(action.payload);
      state.authUser = action.payload;
    },
  },
});

export default loggedIn.reducer;
export const { setLoggedInUser } = loggedIn.actions;

usersSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
  {
    firstName: "matt",
    age: 30,
    total: 0,
  },

  {
    firstName: "mike",
    age: 25,
    total: 0,
  },
  {
    firstName: "steve",
    age: 22,
    total: 0,
  },
];

export const usersReducer = createSlice({
  name: "UsersSlice",
  initialState,
});

export default usersReducer.reducer;

To summarize I need to understand why my authUser is added to my redux store only when the login button does not route to another component

code that adds the authUser

         <Button
              variant="success"
              style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
              onClick={authorizeUser}
            >
              Login
            </Button>

code that does not add the authUser

<Link to="home">
            <Button
              variant="success"
              style={{ width: "20%", marginTop: "3%", marginBottom: "1%" }}
              onClick={authorizeUser}
            >
              Login
            </Button>
</Link>

1

There are 1 best solutions below

0
On

Issue

The issue here is that enqueued React state updates are asynchronously processed. The navigation action to "/home" when wrapping the login button with a Link occurs well before the enqueued state update is processed. React sees that the UserLogin component is no longer mounted and ignores (silently) the state update.

Solution

I suggest moving the "login" logic into an asynchronous action. This will return a Promise to the UserLogin component that can be waited on and upon successful authentication issue the navigation action to the "/home" path.

Example:

loggedInSlice:

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

const initialState = {
  authUser: []
};

const loginUser = createAsyncThunk(
  "loggedIn/loginHandler",
  async (user, { dispatch }) => {
    ... asynchronous login logic ...

    // if successfully logged in
    dispatch(loggedIn.actions.setLoggedInUser(user));
    return ... response object/success message/etc...;
  }
);

export const loggedIn = createSlice({
  name: "loggedIn",
  initialState,
  reducers: {
    setLoggedInUser: (state, action) => {
      state.authUser = action.payload;
    }
  }
});

export default loggedIn.reducer;
export const { setLoggedInUser } = loggedIn.actions;
export { loginUser };

UserLogin

Dispatch the login action directly in the button's onClick handler, wait for resolved Promise, then navigate.

...
import { loginUser } from "../features/loggedInSlice";

export default function UserLogin() {
  const navigate = useNavigate();

  const [user, setUser] = useState("Select A User");
  const users = useSelector((state) => state.users);

  const state = useSelector((state) => state);
  
  const dispatch = useDispatch();
  
  ...

  function authorizeUser() {
    const selectedUser = users.find((userName) => userName.firstName === user);

    dispatch(loginUser(selectedUser))
      .unwrap()
      .then((response) => {
        console.log({ response });
        navigate("/home");
      });
  }

  ...

  return (
    <div>
      <div ...>
        <Card ...>
          ...
          <div>
            <Button
              ...
              onClick={authorizeUser}
            >
              Login
            </Button>
          </div>
        </Card>
        ...
      </div>
    </div>
  );
}

Edit state-from-redux-store-gets-lost-reset-when-changing-route