Search code examples
javascriptreactjsreduxredux-toolkitrtk-query

Redux toolkit userSlice needs a refresh to access data from localstorage


I'm using Redux Toolkit (RTK) Query for making API calls, using RTK for state management. The issue I'm facing is that every time I log in, the user information that I get from backend isn't automatically updated from the localStorage and needs to be refreshed once before the data is accessed in the userSlice. I'm using extraReducers to automatically fetch the data from userLogin endpoint and storing it in localStorage. I'm again accessing the localStorage data and providing it as the initial state of the userSlice.

This is my User Slice.

import { createSlice } from "@reduxjs/toolkit";
import Apiservice from "../service/apiService";

const initialState = {
  userInfo: localStorage.getItem("userInfo")
    ? JSON.parse(localStorage.getItem("userInfo"))
    : null,
};

const userSlice = createSlice({
  name: "userSlice",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addMatcher(
      Apiservice.endpoints.userLogin.matchFulfilled,
      (state, action) => {
        localStorage.setItem("userInfo", JSON.stringify(action.payload));
      }
    );

    builder.addMatcher(
      Apiservice.endpoints.getMyDetails.matchFulfilled,
      (state, action) => {
        localStorage.setItem("userInfo", JSON.stringify(action.payload));
      }
    );
  },
});

export const {} = userSlice.actions;

export default userSlice.reducer;

This is the Login component where I need to manually call window.location.reload() - not doing which will not give me the userInfo data from userSlice.

const Login = () => {
  const [showPass, setShowPass] = useState(false);
  const [showPass1, setShowPass1] = useState(false);

  const {
    control,
    watch,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      email: "",
      password: "",
    },
  });
  const toast = useToast();
  const navigate = useNavigate();

  const email = watch("email");
  const password = watch("password");

  const [userLogin, { isLoading, isError, isSuccess, error, data }] =
    useUserLoginMutation();

  useEffect(() => {
    if (isSuccess) {
      toast({
        title: "Login Successful.",
        status: "success",
        duration: 9000,
        isClosable: true,
      });
      navigate("/");
      window.location.reload();
    }
    if (isError) {
      toast({
        title: error?.data?.message,
        status: "error",
        duration: 5000,
        isClosable: true,
      });
    }
  }, [isSuccess, toast, navigate, isError, error?.data]);

  const onSubmit = (data) => {
    userLogin(data);
  };

  ...

When I first log in the userInfo data I get using useSelector is empty and gets filled with localStorage data once I refresh.

I'm assuming there's something asynchronous going on with how redux store stores/retrieves information, but I'm confused as to which approach I should take. Also what to do when there's an api call which updates user information? I should refresh every time that happens as well?


Solution

  • You appear to have a bit of a misunderstanding of several key concepts:

    1. Reducer functions are not for issuing side-effects, they should be pure functions that consume a current state value and an action, and return the next state value.
    2. LocalStorage is non-reactive, updating it won't magically trigger a React application to rerender or reload itself just so it can pull in values from localStorage. Update the source of truth in the app's state.

    Ignoring state persistence for now, the reducer functions should just update the state with the value you want the state to have. When the state is updated this will trigger the subscribed (to that state) components to rerender and receive the current userInfo state value.

    Example:

    const initialState = {
      userInfo: localStorage.getItem("userInfo")
        ? JSON.parse(localStorage.getItem("userInfo"))
        : null,
    };
    
    const userSlice = createSlice({
      name: "userSlice",
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder
          .addMatcher(
            Apiservice.endpoints.userLogin.matchFulfilled,
            (state, action) => {
              state.userInfo = action.payload;
            }
          )
          .addMatcher(
            Apiservice.endpoints.getMyDetails.matchFulfilled,
            (state, action) => {
              state.userInfo = action.payload;
            }
          );
      },
    });
    

    In a pinch, you could certainly additionally update the localStorage from the reducer function, but this would be considered anti-pattern by many and should be avoided as much as possible.

    const userSlice = createSlice({
      name: "userSlice",
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder
          .addMatcher(
            Apiservice.endpoints.userLogin.matchFulfilled,
            (state, action) => {
              state.userInfo = action.payload;
              localStorage.setItem("userInfo", JSON.stringify(action.payload));
            }
          )
          .addMatcher(
            Apiservice.endpoints.getMyDetails.matchFulfilled,
            (state, action) => {
              state.userInfo = action.payload;
              localStorage.setItem("userInfo", JSON.stringify(action.payload));
            }
          );
      },
    });
    

    It would be better to issue the side-effect from an action, e.g. in the onQueryStarted property of the API slice's queries/mutations, or simply integrating a redux state persistence solution/library like redux-persist (see also persistence and rehydration).