Expo, Redux Toolkit, set timeout for user slice data (session)

Viewed 1671

I'm using Redux Toolkit for an Expo app. In this app I have a user.js which is a redux toolkit slice. This slice has auth information (user data, access token, refresh token). The thing is that the session on the server expires after 15 minutes.

What my client requires is to logout the user from the app after 15 minutes.

My question is: how can I achieve this in a redux toolkit slice? One way could be adding a logged_at timestamp to my state and for every API request, check the elapsed time (comparing logged_at > now) but I want to make it the right way and I feel this approach is not very eloquent or usable.

How can I dispatch an action to logout the user after X time has passed using redux/redux toolkit?

1 Answers

Easy Way: Selectors

Storing a timestamp sounds right to me. I might prefer to store the expiration timestamp expiresAt instead of loggedAt.

You can add some logic in a selector function which removes the need for an explicit "logout" action. We can leave expired users in the state but verify them before accessing. This function only returns the user object if it is still valid, and null if it has already expired.

const selectUser = (state) => Date.now() > state.user.expiresAt ? null : state.user.user;

Scheduled Dispatch

You could schedule a dispatch using setTimeout, but you would have to do it at the highest level of your app rather than in the component where you dispatch the login. I created a little demo where it logs out automatically after 10 seconds. A useEffect hooks detects changes to an isLoggedIn property from state (could be derived through a selector) and schedules the logout when isLoggedIn becomes true.

const App = () => {
  const isLoggedIn = useSelector((state) => state.user.isLoggedIn);

  const dispatch = useDispatch();

  useEffect(
    () => {
      if (isLoggedIn) {
        setTimeout(
          () => {
            dispatch(logOut());
            alert("Logged Out");
          },
          // log out after 10 seconds
          10 * 1000
        );
      }
    },
    // respond to changes in isLoggedIn
    [dispatch, isLoggedIn]
  );

  return <LogIn />;
};

Redux-Saga

You can schedule a logout in response to a login using redux-saga. You would want to create a non-blocking fork with a delay for 15 minutes. With saga you can also cancel the task if the user logs out on their own before the time is up.

It would be similar to this example for retrying API calls which uses a 2000 ms delay.


Async Thunk

We can schedule a dispatch by using a createAsyncThunk which has a long delay. The user might be already logged out and a different user could be logged in before this resolves. You could handle this by checking the username against the current one either in the reducer or in the thunk.

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

interface User {
  username: string;
}

type UserState = User | null;

const initialState = null as UserState;

const wait = (ms: number) =>
  new Promise<void>((resolve) => {
    setTimeout(() => resolve(), ms);
  });

export const asyncLogIn = createAsyncThunk(
  "user/asyncLogIn",
  async (user: User) => {
    await wait(10 * 1000);
    return user;
  }
);

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    // manual log out
    logOut() {
      return null;
    }
  },
  extraReducers: (builder) =>
    builder
      // log in when thunk is initiated
      .addCase(asyncLogIn.pending, (state, action) => {
        const user = action.meta.arg;
        return user;
      })
      // log out when thunk finally resolves
      .addCase(asyncLogIn.fulfilled, (state, action) => {
        // only log out if this user is still the logged in user
        if (action.payload.username === state?.username) {
          return null;
        }
      })
});

export const { logOut } = userSlice.actions;
export default userSlice.reducer;

Code Sandbox Demo

Related