Check if State is empty when transferring to a new page - reactjs

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.

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);

Why even after fetching the data and logging it, whenever I render it to the page after reloading the application breaks in React?

I am undertaking one of the projects from frontendmentor.io
Whenever I click any of the mapped data, with the help of react-router-dom I am redirecting it to url/countryName.
In this scenario, I am using react-router-dom to fetch a particular country while using the useLocation hook to fetch the url, the problem is everything works fine in the first render but as soon as I reload the website the application breaks. No matter how many times I try it never renders again.
import axios from 'axios'
import React,{ useEffect,useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
const Country = () => {
const location = useLocation()
const name = location.pathname.split('/')[1]
const navigate = useNavigate()
const [country,setCountry] = useState({})
useEffect(() => {
const getCountry = async() => {
try {
const res = await axios.get(`https://restcountries.com/v3.1/name/${name.toLowerCase()}?fullText=true`)
console.log(res.data,res)
setCountry(res.data)
} catch (error) {
console.log(error)
}
}
getCountry()
}, [name])
console.log(country[0].name.common) // returns undefined after reload
const backButton = (event) => {
event.preventDefault()
navigate('/')
}
return (
<div className='country-page page'>
<button onClick={backButton} className='backButton'>back button</button>
<div className='country-page-layout'>
<h2></h2>
</div>
</div>
)
}
export default Country
Other resources:
error message screenshot
API which I am using : https://restcountries.com/
You need to wait for your data to be fetched first, and then render it.
const [country,setCountry] = useState({})
//country is an empty object
//but here, your are trying to get the value from the array. (But country is an empty object)
console.log(country[0].name.common) // returns undefined after reload
The solution is to check if your data is here first
if (country?.[0]?.name?.common){
console.log(country[0].name.common)
}

React Context data is empty after routed to next Page

I have a small dummy react application, I have created a "Home Page" after the user is logged on the home page context is set for user credentials from the home page and I am able to get it on the home page. but I have created another page "profile" after I change the route to profile in URL, there is no longer data. I understand that since the page is refreshed data is lost because data persistence is the job of databases. but if I have to query some common data for every page what advantage does context put on the table? thanks.
On Home Page below the code, I wrote to set user credentials.
Home.js
**const { userCredentails, setUserCredentails } = useUserContext();**
useEffect(() => {
const getInitialData = async () => {
const returnData = await AuthFunction();
if (returnData.success) {
**setUserCredentails(returnData.user);**
return dispatch({ type: true, userData: returnData.user });
}
return dispatch({ type: false });
};
getInitialData();
}, []);
Now if a want to access the same data on the profile I don't want to query the database how do get it from there.
**const cntx = useContext(userCredentialsContext);**
above code returns empty objects.
finally, this is the context.js page
import { useState, createContext, useContext } from "react";
export const userCredentialsContext = createContext({});
export const UserContextProvider = ({ children }) => {
const [userCredentails, setUserCredentails] = useState({});
return (
<userCredentialsContext.Provider
value={{ userCredentails, setUserCredentails }}
>
{children}
</userCredentialsContext.Provider>
);
};
export const useUserContext = () => {
const data = useContext(userCredentialsContext);
return data;
};
if you implement your routing by using an anchor tag() or window.location, then the whole page will refresh including your context, setting it back to default state, the right way to do it is by using the Link tag from react-router-dom, here is an example;
import { Link } from "react-router-dom";
<Link to="/home">Home</Link>

Send Id to another component after signin user react js

I used Firebase for user authentication. I set functions for register users. then I created a signin function. When the user signs user redirects to user profile edit mode. Now I want to retrieve data when signing in user his/her particular data. I need to get a firebase user id to pass the another component when sign in. I try to get firebase user id and pass id to another component.Then I try to pass id to the backend using get function. Then I think i can retrive data for paticular user
signin function
async onSubmit(e) {
e.preventDefault();
const email = this.state.todo_email;
const password = this.state.todo_password;
try {
const signInresponse = await firebaseAuth.signInWithEmailAndPassword(email, password);
history.push('/User/Directory');
} catch (e) {
console.error(e);
}
Edit
I may have misread your question 😬
Your signInresponse should return a UserCredential which has a user property. The user prop can then be used to grab a bunch of other props.
So, signInresponse.user.uid will get you the uid...
I personally like the Promise structure and I destructure the user out, because it feels more event-driven.
firebaseAuth.signInWithEmailAndPassword(email, password)
.then(({ user }) => {
// do stuff
setUID(user.uid);
setUserName(user.displayName);
})
.catch((error) => {
handleWarning(error);
});
As an aside, check out the react-firebase-hooks. It can make things a little cleaner.
I hope this better answers your question.
You could use React.Context as a hook and expose it to your app as need. The usage is exactly like the useState() hook.
The main Context hook can be in a file:
// UserContext.js
import { createContext } from "react";
const userContextProvider = createContext({
userContext: {
name: null,
uid: null,
},
setUserContext: () => {},
});
export default userContextProvider;
In your base-level App you import the hook above, and wrap your project:
// App.js
import userContextProvider from "./api/UserContext";
const [userContext, setUserContext] = useState({ name: null, uid: null });
const value = { userContext, setUserContext };
return (
<userContextProvider.Provider value={value}>
<Router>
<Route path={} component={} ...
// ...
And then in the components you want to set or read your uid or name or whatever value:
// RandomComponent.js
import React, { useState, useContext } from "react";
import userContextProvider from "./api/UserContext";
const { userContext, setUserContext } = useContext(userContextProvider);
Then you can use userContext as your getter and setUserContext as your setter just like useState()

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

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])

Resources