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