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

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?

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

How to logout user in React using context API

I am working with React hooks and created login logout functionality, but now I want user to logout when token is expired.
I am on a welcome page and using it, after two minutes if I do something else and the token is expired, I want to logout the user
I have created a context, Auth context where I have login logout context
I just want to call the logout function whenever the token expires
My authContext
const initialstate = {
user: null,
};
if (localStorage.getItem("JWT_Token")) {
const jwt_Token_decoded = Jwt_Decode(localStorage.getItem("JWT_Token"));
console.log(jwt_Token_decoded.exp * 1000);
console.log(Date.now());
if (jwt_Token_decoded.exp * 1000 < Date.now()) {
localStorage.clear(); // this runs only when I refresh the page or reload on route change it dosent work
} else {
initialstate.user = jwt_Token_decoded;
}
}
const AuthContext = createContext({
user: null,
login: (userData) => {},
logout: () => {},
});
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN":
return {
...state,
user: action.payload,
};
case "LOGOUT":
return {
...state,
user: null,
};
default:
return state;
}
};
const AuthProvider = (props) => {
const [state, dispatch] = useReducer(AuthReducer, initialstate);
const login = (userData) => {
localStorage.setItem("JWT_Token", userData.token);
dispatch({
type: "LOGIN",
payload: userData,
});
};
const logout = () => {
localStorage.clear();
dispatch({ action: "LOGOUT" });
};
return (
<AuthContext.Provider
value={{ user: state.user, login, logout }}
{...props}
/>
);
};
export { AuthContext, AuthProvider };
In the above code I am checking for expiry of token, but that only runs when page reloads, here I want to run it in every route change so that I can check for the token expiry.
I don't know how to do that and where to do that.
To logout I just need to call my logout context function, but I don't understand how to make the call.
I don't know If I have to do something in my Axios instance which is a separate file like below. Here I am creating one instance so that I can define my headers and other stuff at one place.
//global axios instance
import axios, { AxiosHeaders } from "axios"; // import axios from axios
const BASE_URL = "https://jsonplaceholder.typicode.com"; // server api
export default axios.create({
baseURL: BASE_URL,
headers: {
"Content-Type": "Application/json",
Access-token: "token here",
},
});
How can I approach this problem? I checked this question but in this example GraphQL has been used, so there is a function to set context where I can pass and use the store to dispatch my logout.
I have shared my axiosInstance code I think something needs to be added there. I am ready to use any approach that will generate some middleware, so that I can check for token in one place.
There are some points to consider in your approach
You can update the log out function to send the user to the login page, this way you guarantee this behavior whenever this user has been logged out;
Use the API response to validate when your token has expired, this way you don't need to keep watching the time of the token expiration and logout the user after any unauthorized API call;
Pay attention to this localStorage.clear() call, sometimes it can clear more than you want, and it is always a good idea to declare what you really want to clear. Eg.: sometimes you want to keep your user language, theme, or some UI configs that shouldn't be cleared after a logout;
Axios has an API to intercept your API call, this is a good way to do something before/after an API call (use it to logout after any unauthorized response)
If you really need to take care of this logout by the client and don't need to await the API response, you can try the setTimeout method (not recommended)

Firebase auth with custom user fields: React custom hook to manage user state

I'm trying to implement authentication in my app using Firebase and I need to store some custom user fields (e.g. schoolName, programType, etc.) on the user documents that I'm storing in Firestore. I want to have these custom fields in my React state (I'm using Recoil for state management), and I'm very unsure of the best way to do this.
I currently have a Cloud Function responsible for creating a new user document when new auth users are created, which is great, however, I'm having trouble figuring out a good way to get that new user (with the custom fields) into my state, so I came up with a solution but I'm not sure if it's ideal and would love some feedback:
I define the firebase/auth functions (e.g. signInWithPopup, logout, etc.) in an external static file and simply import them in my login/signup forms.
To manage the user state, I created a custom hook useAuth:
const useAuth = () => {
const [user] = useAuthState(auth); // firebase auth state
const [currentUser, setCurrentUser] = useRecoilState(userState); // global recoil state
useEffect(() => {
// User has logged out; firebase auth state has been cleared; so clear app state
if (!user?.uid && currentUser) {
return setCurrentUser(null);
}
const userDoc = doc(firestore, "users", user?.uid as string);
const unsubscribe = onSnapshot(userDoc, (doc) => {
console.log("CURRENT DATA", doc.data());
if (!doc.data()) return;
setCurrentUser(doc.data() as any);
});
if (currentUser) {
console.log("WE ARE UNSUBBING FROM LISTENER");
unsubscribe();
}
return () => unsubscribe();
}, [user, currentUser]);
};
This hook uses react-firebase-hooks and attempts to handle all cases of the authentication process:
New users
Existing users
Persisting user login on refresh (the part that makes this most complicated - I think)
To summarize the above hook, it essentially listens to changes in firebase auth state via useAuthState, then I add a useEffect which creates a listener of the user document in firestore, and when that user has successfully been inputted into the db by the Cloud Function, the listener will fire, and it will populate recoil state with doc.data() (which contains the custom fields) via setCurrentUser. As for existing users, the document will already exist, so a single snapshot will do the trick. The rationale behind the listener is the case of new users, where a second snapshot will be required as the first doc.data() will be undefined even though useAuthState will have a user in it, so it's essentially just waiting for the Cloud Function to finish.
I call this hook immediately as the app renders to check for a Firebase Auth user in order to persist login on refresh/revisit.
I've been messing around on this for quite some time, and this outlined solution does work, but I have come up with multiple solutions so I would love some guidance.
Thank you very much for reading.
Step 1: Define CurrentUser, and UserProfile states
import { atom, selector } from "recoil";
import { type User } from "firebase/auth";
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
});
export const UserProfile = atomFamily<Profile | null, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return undefined;
}
});
Step 2: Listen to the authenticated user state changes
export const CurrentUser = atom<User | null | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
defaultValue: undefined,
effects: [
(ctx) => {
if (ctx.trigger === "get") {
// Import Firebase App instanced defined in a separate chunk
const promise = import("../core/firebase")
.then((fb) =>
fb.auth.onAuthStateChanged((user) => {
ctx.setSelf(user);
})
)
.catch((err) => ctx.setSelf(Promise.reject(err)));
return () => promise.then((unsubscribe) => unsubscribe?.());
}
},
],
});
Step 3: Load user profile by Firebase user UID
export const UserProfile = atomFamily<User | null | undefined, string | undefined>({
key: "CurrentUser",
dangerouslyAllowMutability: true,
get(uid) {
return async function() {
if (!uid) return null;
import("../core/firebase").then(({ fs }) => {
// TODO: Retrieve Firestore document with the user profile
return getDoc(doc(dollection(fs, "users"), uid));
});
};
}
});
Step 4: Add React hooks
import { useRecoilValue } from "recoil";
export function useCurrentUser() {
return useRecoilValue(CurrentUser);
}
export function useCurrentUserProfile() {
const me = useRecoilValue(CurrentUser);
return useRecoilValue(UserProfile(me?.uid));
}
Usage Example
import { useCurrentUser, useCurrentUserProfile } from "../state/firebase";
export function Example(): JSX.Element {
const me = useCurrentUser(); // Firebase user object
const profile = useCurrentUserProfile(); // Custom profile from Firestore
}
See https://github.com/kriasoft/cloudflare-starter-kit for a working example

Pros & cons of using extra reducers vs dispatching within async thunk

I'm rather new to the whole React & Redux ecosystem & am trying to understand when & why to use extra reducers vs directly dispatching actions within an async thunk when working with the Redux toolkit.
Probably best explained with an example showing both solutions:
Version 1: Using extra reducers
auth.slice.ts
// ...
export const login = createAsyncThunk<LoginResponse, LoginData>(
'auth/login',
async ({ email, password }, thunkAPI) => {
const data = await AuthService.login(email, password);
// Extract user info from login response which holds other information as well
// in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(data);
LocalStorageService.storeUserInfo(userInfo);
// Return the whole login response as we're interested in the other data
// besides the user info in other slices which handle `login.fulfilled` in
// their own `extraReducers`
return data;
}
);
// ...
const authSlice = createSlice({
// ...
extraReducers: builder => {
builder.addCase(login.fulfilled, (state, { payload }) => {
// Again: Extract user info from login response which holds other
// information as well in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(payload);
return { ...state, userInfo };
}))
// ...
},
});
// ...
Version 2: Using dispatch inside async thunk
auth.slice.ts
// ...
export const login = createAsyncThunk<LoginResponse, LoginData>(
'auth/login',
async ({ email, password }, thunkAPI) => {
const data = await AuthService.login(email, password);
// Extract user info from login response which holds other information as well
// in which we're not interested in the auth slice...
const userInfo = loginResponseToUserInfo(data);
LocalStorageService.storeUserInfo(userInfo);
// !!! Difference to version 1 !!!
// Directly dispatch the action instead of using `extraReducer` to further
// process the extracted user info
thunkAPI.dispatch(authSlice.actions.setUserInfo(userInfo));
// Return the whole login response as we're interested in the other data
// besides the user info in other slices which handle `login.fulfilled` in
// their own `extraReducers`
return data;
}
);
// ...
const authSlice = createSlice({
// ...
reducers: {
setUserInfo: (state, { payload }: PayloadAction<UserInfo>) => ({
...state,
userInfo: payload,
}),
// ...
},
});
// ...
Question
If I'm not completely wrong, both examples do the exact same thing but looking through the internet I mostly find people suggesting option 1 using the extraReducer which is why I'm asking:
Are both versions basically ok/correct or am I missing something?
Are there any benefits of sticking to the "extraReducers approach"?
One minor drawback in this specific example is that I have to perform the conversion with loginResponseToUserInfo in 2 places (the async thunk & the extraReducer) whilst I only need to call it once in the 2nd version...
In my opinion both are valid although I would go for #1 personally.
To justify my choice :
you should consider 'actions' as 'events', and the event is 'user logon successfull'. Having explicit action to set data into a slice is kinda wrong pattern
consider each of your slice as sub modules that should be able to work independently. The module in charge of authentication should not care if other slices are listening to its event or not ; in the future you might have other slices intereseted in this event and you dont want to end up with several extraneous dispatch in your thunk + the 'logon success' event might be triggered from another source.

How can I cache data that I already requested and access it from the store using React and Redux Toolkit

How can I get data from the store using React Redux Toolkit and get a cached version if I already requested it?
I need to request multiple users for example user1, user2, and user3. If I make a request for user1 after it has already been requested then I do not want to fetch user1 from the API again. Instead it should give me the info of the user1 from the store.
How can I do this in React with a Redux Toolkit slice?
Edit: This answer predates the release of RTK Query which has made this task much easier! RTK Query automatically handles caching and much more. Check out the docs for how to set it up.
Keep reading if you are interested in understanding more about some of the concepts at play.
Tools
Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.
createEntityAdapter allows us to store and select entities like a user object in a structured way based on a unique ID.
createAsyncThunk will create the thunk action that fetches data from the API.
createSlice or createReducer creates our reducer.
React vs. Redux
We are going to create a useUser custom React hook to load a user by id.
We will need to use separate hooks in our hooks/components for reading the data (useSelector) and initiating a fetch (useDispatch). Storing the user state will always be the job of Redux. Beyond that, there is some leeway in terms of whether we handle certain logic in React or in Redux.
We could look at the selected value of user in the custom hook and only dispatch the requestUser action if user is undefined. Or we could dispatch requestUser all the time and have the requestUser thunk check to see if it needs to do the fetch using the condition setting of createAsyncThunk.
Basic Approach
Our naïve approach just checks if the user already exists in the state. We don't know if any other requests for this user are already pending.
Let's assume that you have some function which takes an id and fetches the user:
const fetchUser = async (userId) => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return res.data;
};
We create a userAdapter helper:
const userAdapter = createEntityAdapter();
// needs to know the location of this slice in the state
export const userSelectors = userAdapter.getSelectors((state) => state.users);
export const { selectById: selectUserById } = userSelectors;
We create a requestUser thunk action creator that only executes the fetch if the user is not already loaded:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const existing = selectUserById(getState(), userId);
return !existing;
}
}
);
We can use createSlice to create the reducer. The userAdapter helps us update the state.
const userSlice = createSlice({
name: "users",
initialState: userAdapter.getInitialState(),
reducers: {
// we don't need this, but you could add other actions here
},
extraReducers: (builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
});
export const userReducer = userSlice.reducer;
But since our reducers property is empty, we could just as well use createReducer:
export const userReducer = createReducer(
userAdapter.getInitialState(),
(builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
)
Our React hook returns the value from the selector, but also triggers a dispatch with a useEffect:
export const useUser = (userId: EntityId): User | undefined => {
// initiate the fetch inside a useEffect
const dispatch = useDispatch();
useEffect(
() => {
dispatch(requestUser(userId));
},
// runs once per hook or if userId changes
[dispatch, userId]
);
// get the value from the selector
return useSelector((state) => selectUserById(state, userId));
};
isLoading
The previous approach ignored the fetch if the user was already loaded, but what about if it is already loading? We could have multiple fetches for the same user occurring simultaneously.
Our state needs to store the fetch status of each user in order to fix this problem. In the docs example we can see that they store a keyed object of statuses alongside the user entities (you could also store the status as part of the entity).
We need to add an empty status dictionary as a property on our initialState:
const initialState = {
...userAdapter.getInitialState(),
status: {}
};
We need to update the status in response to all three requestUser actions. We can get the userId that the thunk was called with by looking at the meta.arg property of the action:
export const userReducer = createReducer(
initialState,
(builder) => {
builder.addCase(requestUser.pending, (state, action) => {
state.status[action.meta.arg] = 'pending';
});
builder.addCase(requestUser.fulfilled, (state, action) => {
state.status[action.meta.arg] = 'fulfilled';
userAdapter.upsertOne(state, action.payload);
});
builder.addCase(requestUser.rejected, (state, action) => {
state.status[action.meta.arg] = 'rejected';
});
}
);
We can select a status from the state by id:
export const selectUserStatusById = (state, userId) => state.users.status[userId];
Our thunk should look at the status when determining if it should fetch from the API. We do not want to load if it is already 'pending' or 'fulfilled'. We will load if it is 'rejected' or undefined:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const status = selectUserStatusById(getState(), userId);
return status !== "fulfilled" && status !== "pending";
}
}
);

How to share redux state client-side and props server-side in Next JS

I'm a newbie with Next JS.
I use Next JS and Redux.
I have a short code below:
const AdminContainer = (props) => {
return (
<AdminMasterView>
<DashboardView studentList={props.studentListServer}/>
</AdminMasterView>
)
}
export const getStaticProps = (async () => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList
});
const mapDispatchToProps = {
getStudentRegisterAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
I also have studentList (array type) props is declare in Redux. I want to use it to pass data because I have many tasks to do with data such as filter, order,...
Is there any way to use studentList like this and my app still is server rendering first time.
If I dispatch studentListServer to studentList, it still work. But my app isn't server rendering.
<DashboardView studentList={props.studentList}/>
Or easier, I'll check to use props.studentList for client-side and props.studentListServer for server-side. But I think it's not good.
Thank you so much!
You could use the next-redux-wrapper package. It allows to sync a Redux state on server and client. Consider the example:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
// dispatch the action that saves the data
store.dispatch({ type: 'SET_STUDENTS', payload: response });
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
wrapper.getStaticProps wraps your getStaticProps function with the new parameter store that is a Redux store in fact.
Action with type SET_STUDENTS sets the student list on a server side. When Next.js generates the page, it will save this data in static JSON. So when the page opens on client side, next-redux-wrapper recreates a state dispatching HYDRATE action with saved on a build time static JSON that you can use to restore the studentInfoReducers reducer.
E.g. in your reducer you should implement something like:
import { HYDRATE } from 'next-redux-wrapper';
const initialState = { studentList: [] };
// studentInfoReducers reducer
function reducer(state = initialState, action) {
// this sets your student list
if (action.type === 'SET_STUDENTS') {
return {
...state,
studentList: action.payload,
};
}
// this rehydrates your store from server on a client
if (action.type === HYDRATE) {
return action.payload.studentInfoReducers;
}
return state;
}
So afterwards you should have a valid synced state on client and server at the same time:
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList // works on server and client
});
Let me know if you have any questions, next-redux-wrapper can be tricky from a first look.
You don't need to use Redux for that.
Using just cookies you can achieve bidirectional communication, see https://maxschmitt.me/posts/next-js-cookies/
Another example:
Client to Server: manually set a cookie in the client side and then read it in the server with req.headers.cookie or some library like 'cookie'
Server to Client: just read the cookie, and return what you need as a regular prop or update the cookie.
import { useState, useEffect } from "react";
import Cookie from "js-cookie";
import { parseCookies } from "../lib/parseCookies";
const Index = ({ initialRememberValue = true }) => {
const [rememberMe, setRememberMe] = useState(() =>
JSON.parse(initialRememberValue)
);
useEffect(() => {
//save/create the cookie with the value in the client
Cookie.set("rememberMe", JSON.stringify(rememberMe));
}, [rememberMe]);
return (
<div>
remember me
<input
type="checkbox"
value={rememberMe}
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
/>
</div>
);
};
Index.getInitialProps = ({ req }) => {
//read the cookie on the server
const cookies = parseCookies(req); //parseCookies is a simple custom function you can find
return {
//send the value as a regular prop
initialRememberValue: cookies.rememberMe
};
};
export default Index;
Reference: https://github.com/benawad/nextjs-persist-state-with-cookie/blob/master/pages/index.js

Resources