javascriptreactjsreact-reduxdispatchnext.js13

Redux doesn't return to my initial state, when logout


After my redux dispatches the logout action, I want to reset to my initial state. I'm using nextjs, and I don't understand why my desired logout reducer in authenticateSlice.js doesn't execute. I was also using a redux-persist to keep my state persistent.

Here is my code from store.js

import { configureStore } from "@reduxjs/toolkit";
import authenticateSlice from "./slices/authenticateSlice";
import { persistReducer, persistStore } from "redux-persist"; // import persistStore
import storage from "./storage";
import { encryptTransform } from "redux-persist-transform-encrypt";
import { combineReducers } from "redux";
import thunk from "redux-thunk";

const reducers = combineReducers({
  auth: authenticateSlice.reducer,
});

let transforms = null;

if (process.env.NODE_ENV === "production") {
  const encrypt = encryptTransform({
    secretKey: process.env.NEXT_PUBLIC_REDUX_SECRET,
    onError: function (error) {
      // Handle the error.
    },
  });
  transforms = [encrypt];
}

const persistConfig = {
  key: "root",
  storage: storage,
  transforms,
};

const persistedReducer = persistReducer(persistConfig, reducers);

const store = configureStore({
  reducer: persistedReducer,
  middleware: [thunk],
});

let persistor = persistStore(store); // initialize persistor

export { store, persistor };

Here is my code from the authenticateSlice.js which I have the logout function to dispatch.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { commonPostRequest } from "@/django-api/common/commonRequestAPI";
import { CommonPostHeader } from "@/django-api/common/commonHeadersAPI";

export const fetchUserLogin = createAsyncThunk(
  "auth/login",
  async (arg, { rejectWithValue }) => {
    const { username, password } = arg;
    try {
      const loginRequest = await commonPostRequest({
        body: { username: username, password: password },
        headers: CommonPostHeader,
        url: "http://localhost:8000/api/accounts_app/login",
      });
      if (loginRequest.data) {
        return loginRequest.data;
      } else {
        throw new Error(loginRequest.message);
      }
    } catch (err) {
      return rejectWithValue(err.message);
    }
  }
);

export const logout = () => {
  return { type: "auth/logout" };
};

// loading: 'idle' | 'pending' | 'succeeded' | 'failed'
const initialState = {
  data: {},
  loading: "idle",
  message: "",
};

const authenticateSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserLogin.pending, (state) => {
        state.loading = "pending";
      })
      .addCase(fetchUserLogin.fulfilled, (state, action) => {
        state.loading = "succeeded";
        state.data = action.payload;
      })
      .addCase(fetchUserLogin.rejected, (state, action) => {
        state.loading = "failed";
        state.message = action.payload;
      })
      .addCase(logout, (state) => {
        Object.assign(state, initialState);
      });
  },
});

export default authenticateSlice;

Lastly, here is my Header function component where I want to dispatch the logout action from slice or dispatch the logout function to return with my initial state.

import { Typography } from "@mui/material";
import { Row, Col, Divider, notification } from "antd";
import styles from "./Header.module.scss";
import Image from "next/image";
import Link from "next/link";
import {
  toLocalZoneDateTime,
  getCurrentDateTimeStamp,
} from "../common/functions/datetime";
import { useDispatch } from "react-redux";
import { useRouter } from "next/router";
import { logout } from "../../next-redux/slices/authenticateSlice";

const Header = (props) => {
  const dispatch = useDispatch();
  const { user } = props;
  const router = useRouter();

  const currentDateTimeStamp = getCurrentDateTimeStamp();

  const handleLogoutClick = (e) => {
    e.preventDefault();
    console.log("logout");
    dispatch(logout());
  };

  return (
    <>
      <Row>
        <Col span={24}>
          <Row gutter={[8, 8]} className={styles.topBar}>
            <Col span={12}>
              <Typography variant="subtitle2" className={styles.topDate}>
                {toLocalZoneDateTime(currentDateTimeStamp)}
              </Typography>
            </Col>
            <Col span={12}>
              {user ? (
                <Typography align="right">
                  <a
                    onClick={(e) => {
                      handleLogoutClick(e);
                    }}
                    className={styles.topBarLinks}
                  >
                    Logout
                  </a>
                </Typography>
              ) : (
                <>
                  <Typography align="right">
                    {" "}
                    <Link href="/signin/" className={styles.topBarLinks}>
                      Sign-in
                    </Link>{" "}
                    <Link href="/signup/" className={styles.topBarLinks}>
                      Sign-up
                    </Link>
                  </Typography>
                </>
              )}
            </Col>
          </Row>
        </Col>
      </Row>
    </>
  );
};

export default Header;

Solution

  • Issue

    logout is a function, not an action object, so the extra reducer case isn't running.

    export const logout = () => {
      return { type: "auth/logout" };
    };
    
    ...
    
    .addCase(logout, (state) => { // <-- function, e.g. logout.type undefined
      Object.assign(state, initialState);
    });
    
    

    Solution

    The solution would be to declare logout as a created action via the createAction utility.

    import { createSlice, createAction, createAsyncThunk } from "@reduxjs/toolkit";
    import { commonPostRequest } from "@/django-api/common/commonRequestAPI";
    import { CommonPostHeader } from "@/django-api/common/commonHeadersAPI";
    
    export const fetchUserLogin = createAsyncThunk(
      ...
    );
    
    export const logout = createAction("auth/logout");
    
    const initialState = { ... };
    
    const authenticateSlice = createSlice({
      name: "auth",
      initialState,
      extraReducers: (builder) => {
        builder
          .addCase(fetchUserLogin.pending, (state) => {
            state.loading = "pending";
          })
          .addCase(fetchUserLogin.fulfilled, (state, action) => {
            state.loading = "succeeded";
            state.data = action.payload;
          })
          .addCase(fetchUserLogin.rejected, (state, action) => {
            state.loading = "failed";
            state.message = action.payload;
          })
          .addCase(logout, (state) => {
            return initialState;
          });
      },
    });
    
    export default authenticateSlice.reducer;
    

    Or declare a logout reducer case which will generate the logout action that could be exported.

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
    import { commonPostRequest } from "@/django-api/common/commonRequestAPI";
    import { CommonPostHeader } from "@/django-api/common/commonHeadersAPI";
    
    export const fetchUserLogin = createAsyncThunk(
      ...
    );
    
    const initialState = { ... };
    
    const authenticateSlice = createSlice({
      name: "auth",
      initialState,
      reducers: {
        logout: (state) => {
          return initialState;
        },
      },
      extraReducers: (builder) => {
        builder
          .addCase(fetchUserLogin.pending, (state) => {
            state.loading = "pending";
          })
          .addCase(fetchUserLogin.fulfilled, (state, action) => {
            state.loading = "succeeded";
            state.data = action.payload;
          })
          .addCase(fetchUserLogin.rejected, (state, action) => {
            state.loading = "failed";
            state.message = action.payload;
          });
      },
    });
    
    export const { logout } = authenticateSlice.actions;
    
    export default authenticateSlice.reducer;