In my React app, why isn't this Redux reducer executed? - reactjs

Good afternoon,
After doing the React tutorial, and read all the React guides, I did the Redux tutorial to re-write my working auth component to use Redux instead of component state (I also switched from classes to functional components).
My Redux setup is as follow :
import { createSlice } from '#reduxjs/toolkit';
const sessionInitialState = {
authToken: null,
userLogin: null,
};
export const sessionSlice = createSlice({
name: 'session',
initialState: {
value: sessionInitialState,
},
reducers: {
sessionRegister: (state, action) => {
console.log("Storing:")
console.log(action.payload)
state.value = action.payload;
},
sessionDestroy: state => {
console.log("Destroying session")
state.value = sessionInitialState;
localStorage.removeItem("sessionData");
},
},
});
export const { sessionRegister, sessionDestroy } = sessionSlice.actions;
export const selectSession = state => state.session.value;
export default sessionSlice.reducer;
In my login component, I store the auth token and the username as follow :
// [...]
import { useDispatch } from 'react-redux';
import { sessionRegister } from './SessionSlice';
// [...]
function LoginForm(props) {
const dispatch = useDispatch();
function onFormSubmit(event) {
// [...] This is when API response code is 200:
let sessionData = {
authToken: response.data.token,
userLogin: userName,
}
localStorage.setItem("sessionData", JSON.stringify(sessionData));
console.log("Stored:" + localStorage.getItem("sessionData"));
setApiError(null);
dispatch(sessionRegister(sessionData));
And my App component displays the login form if no data is stored in Redux, or the user name and token if they're present :
function App(props) {
const sessionData = useSelector(selectSession);
const storedSessionData = localStorage.getItem("sessionData");
if (storedSessionData && !sessionData.authToken) {
console.log("App init check. Storing :");
console.log(JSON.parse(storedSessionData));
sessionRegister(JSON.parse(storedSessionData));
} else {
if (!storedSessionData) console.log("No sessionData and no stored data");
}
return (
<div className="App">
<div className="topbar"><Topbar /></div>
<div className="content">
{sessionData.authToken ? (
<span>User: {sessionData.userLogin} {storedSessionData}</span>
) : (
<LoginForm />
)
}
</div>
</div>
);
}
All this almost works. The login component stores the data via Redux after the API call, and in localStorage too. The userLogin from Redux is displayed by the App, and the localStorage content too. But if I refresh the page, the App gets the data from locaStorage but sessionRegister is not called (or does nothing).
It happens as follow :
Fisrt open of the page. Console: No sessionData and no stored data
Login via LoginForm. Console:
Localy stored:{"authToken":"5467c25e000df49a1161c4ff8ga1f610053f62b8","userLogin":"testuser"}
Storing in Redux:
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" }
Now the App component correctly renders the content instead of the login form :
User: testuser {"authToken":"5467c25e000df49a1161c4ff8ga1f610053f62b8","userLogin":"testuser"}
So far, so good. But if I refresh the page, the login form is displayed again. In the console I get :
App init check. Storing : App.js:15
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" } App.js:16
App init check. Storing : App.js:15
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" } App.js:16
I don't understand why sessionRegister(JSON.parse(storedSessionData)); is not correctly executed, and why do I get twice the console log App init check.
Thanks for having read all this, any help would be appreciated.

I think you're missing a dispatch
Instead of
sessionRegister(JSON.parse(storedSessionData));
it should be
dispatch(sessionRegister(JSON.parse(storedSessionData)));
in your App component. You get access to dispatch with a hook
const dispatch = useDispatch()
last, but not least, you're updating the store in every render, so that's why you see the log twice. I think you should probably move this to an Effect
const dispatch = useDispatch()
const sessionData = useSelector(selectSession);
useEffect(() => {
const storedSessionData = localStorage.getItem("sessionData");
if (storedSessionData && !sessionData.authToken) {
console.log("App init check. Storing :");
console.log(JSON.parse(storedSessionData));
sessionRegister(JSON.parse(storedSessionData));
} else {
if (!storedSessionData) console.log("No sessionData and no stored data");
}
}, [dispatch, sessionData])

Related

Nexts.js 13 + Supabase > What's the proper way to create a user context

I'm building an app with Next.js 13 and Supabase for the backend, and I've been stuck on figuring out the best/proper way to go about creating a context/provider for the current logged in user.
The flow to retrieve the user from Supabase is this:
Sign in with an OAuth Provider.
Grab the user ID from the session from the supabase onAuthState Changed hook.
Fetch the full user object from the supabase DB with the user ID mentioned above.
I have a supabase listener in my layout that listens for the auth state changes, and works well for setting and refreshing current session.
My initial approach was to add the fetchUser call from within the onAuthState changed hook, however I was running into late update hydration errors.
Taken directly from the examples, this is how the app looks:
// layout.tsx
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
});
const {
data: { session },
} = await supabase.auth.getSession();
return (
<html>
<head />
<body>
<NavMenu session={session} />
<SupabaseListener accessToken={session?.access_token} />
{children}
</body>
</html>
);
}
// supabase-listener.tsx
// taken directly from the supabase-auth-helpers library.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import supabase from "../lib/supabase/supabase-browser";
export default function SupabaseListener({
accessToken,
}: {
accessToken?: string;
}) {
const router = useRouter();
useEffect(() => {
supabase.auth.onAuthStateChange(async (event, session) => {
if (session?.access_token !== accessToken) {
router.refresh();
}
});
}, [accessToken, router]);
return null;
}
I basically just need to wrap my root layout with a LoggedInUserProvider, make the fetch user call somewhere in the initial page load, and set the state of the current logged in user provider.
The other approaches I tried was making the fetch user call from the root layout, and having a LoggedInUserListener client component that takes the user as a property and simply sets the state if the profile exists. This was causing improper set state errors.
Thank you so much.
Check out this PR for a better example of how to structure the application and add a provider for sharing a single instance of Supabase client-side, as well as the session from the server 👍
If you follow a similar pattern, then your additional query for the full user record should go immediately after you get the session in examples/nextjs-server-components/app/layout.tsx. You could then pass this as a prop to the <SupabaseProvider /> and share it across the application from context's value prop.
I am following your awesome auth-helpers example but my context from the provider keeps coming back as null for user details. Is there anything wrong with the code below or is there some isLoading logic that will work better for getting that data?
Also want to confirm, does the SupabaseProvider in the root layout pass down to all other child layout components?
'use client';
import type { Session } from '#supabase/auth-helpers-nextjs';
import { createContext, useContext, useState, useEffect } from 'react';
import type { TypedSupabaseClient } from 'app/layout';
import { createBrowserClient } from 'utils/supabase-client';
import { UserDetails, CompanyDetails } from 'models/types';
type MaybeSession = Session | null;
type SupabaseContext = {
supabase: TypedSupabaseClient;
session: MaybeSession;
userDetails: UserDetails | null;
isLoading: boolean;
};
// #ts-ignore
const Context = createContext<SupabaseContext>();
//TODO get stripe subscription data
export default function SupabaseProvider({
children,
session
}: {
children: React.ReactNode;
session: MaybeSession;
}) {
const [supabase] = useState(() => createBrowserClient());
const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
const [isLoading, setLoading] = useState(false);
// Hydrate user context and company data for a user
useEffect(() => {
const fetchUserDetails = async () => {
if (session && session.user) {
setLoading(true);
const { data } = await supabase
.from('users')
.select('*, organizations (*)')
.eq('id', session.user.id)
.single();
//TODO fix types
setUserDetails(data as any);
setLoading(false);
}
};
if (session) {
fetchUserDetails();
}
}, [session, supabase]);
return (
<Context.Provider value={{ supabase, session, userDetails, isLoading }}>
<>{children}</>
</Context.Provider>
);
}
export const useSupabase = () => useContext(Context);

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

Check if State is empty when transferring to a new page

This is my /chat page this is called from the /login page. I pass this chat page some data with
// this is inside of the page /login
history.push({
pathname: '/chat',
state: {
email: email,
name: password,
},
})
The problem is when I access /chat from /login everything works, but when I access /chat only I get the error TypeError: Cannot destructure property 'email' of 'state' as it is undefined.
Is there an option to query if state is empty? And if so take null?
import React, { useState, useEffect } from "react";
import axios from 'axios';
import { useLocation } from "react-router-dom";
function Chat() {
const { state } = useLocation();
const { email } = state;
const [person, setPerson] = useState([]);
const test_test = { email }
useEffect(() => {
axios.get('localhost:8000/'.concat('FILLER_', email.toString()))
.then((res) => {
const datapersons = res.data;
setPerson( datapersons );
})
.catch(error => {
console.log(error)
})
}, []);
return (
<div>
<h1>Welcome {person.givenname}</h1>
</div>
);
}
export default Chat
Check whether state exists before accessing it:
const email = state == null ? null : state.email;
If you're using TypeScript you can use the ?. syntax:
const email = state?.email;
The issue is when you go to /chat by itself you are not passing the link data to the new route anymore. This means that when you try to get state from useLocation() it won't be there as you didn't pass the data from login.
A solution I would recommend would be to cache the data in the browser and if you don't have useLocation() state you can then get it from the cached data. When you login you can save the information you want to the local cache here is some more detailed information on how to accomplish that. Wherever you login I would use
window.localStorage.setItem('email', 'userEmail');
and then in your component
const { email } = state ? state : window.localStorage.getItem('email');
And then when you log out make sure to clear out local storage unless you want to keep the user logged in between sessions you could use localStorage to accomplish that.

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

Redux async actions and view updates

I'm trying to implement a login form for a website, it's my first project on React so I'm quite a beginner.
To do so, I use socket.io-client inside my redux reducer.
The thing is, it doesn't update the local props correctly.
Here's the code of my view:
const mapStateToProps = state => {
return {
profile: state.profileReducer.profile
}
}
const mapDispatchToProps = dispatch => {
return {
dispatch: action => {
dispatch(action)
}
}
}
...
handleConnection = () => {
const { profile } = this.props
this.props.dispatch({ type: 'CONNECT_USER' })
}
...
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage)
And here's the reducer's action:
import io from 'socket.io-client'
const host = [SERVER_URL]
const socketConnection = io.connect(host, {path: [PATH], secure: true})
const initialState = {
profile: {
token: null,
username: '',
password: ''
}
}
function profileReducer(state = initialState, action) {
switch(action.type) {
...
case 'CONNECT_USER':
let tempProfile = {...state.profile}
socketConnection.emit('login', tempProfile.username + ';' + tempProfile.password)
socketConnection.on('check', msg => {
if (msg !== null && msg !== '')
tempProfile.token = msg
return {
...state,
profile: tempProfile
}
})
return state
...
}
}
The 'check' socket action return a message containing the user connection token which I need to store to make sure the connection is done and allowed.
The thing is, it doesn't update the store value. If I update directly the reducer's state instead of the temporary profile, it partly works : the view props isn't properly updated but a 'console.log(profile)' in a 'setInterval' inside the 'handleConnection' function shows the token value (but the props inside the Chrome React Inspector isn't updated).
I really don't understand what's going on. I suppose the socket.io 'on' function isn't done before the 'return' of my action but I don't know how to handle it.
Does someone as any idea how I could solve this problem ?
Thanks !
Reducers are always synchronous in nature. If you want to perform an async operation (like the socket connection you are trying to establish) in your reducer then you need to use a middleware like Thunk or Saga to achieve the same.
In your case it is always returning the existing state from the last return statement.

Resources