React Context data is empty after routed to next Page - reactjs

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>

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 does react-query query not work when parameter from router.query returns undefined on refresh?

When page is refreshed query is lost, disappears from react-query-devtools.
Before Next.js, I was using a react and react-router where I would pull a parameter from the router like this:
const { id } = useParams();
It worked then. With the help of the, Next.js Routing documentation
I have replaced useParams with:
import { usePZDetailData } from "../../hooks/usePZData";
import { useRouter } from "next/router";
const PZDetail = () => {
const router = useRouter();
const { id } = router.query;
const { } = usePZDetailData(id);
return <></>;
};
export default PZDetail;
Does not work on refresh. I found a similar topic, but manually using 'refetch' from react-query in useEffects doesn't seem like a good solution. How to do it then?
Edit
Referring to the comment, I am enclosing the rest of the code, the react-query hook. Together with the one already placed above, it forms a whole.
const fetchPZDetailData = (id) => {
return axiosInstance.get(`documents/pzs/${id}`);
};
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {});
};
Edit 2
I attach PZList page code with <Link> implementation
import Link from "next/link";
import React from "react";
import TableModel from "../../components/TableModel";
import { usePZSData } from "../../hooks/usePZData";
import { createColumnHelper } from "#tanstack/react-table";
type PZProps = {
id: number;
title: string;
entry_into_storage_date: string;
};
const index = () => {
const { data: PZS, isLoading } = usePZSData();
const columnHelper = createColumnHelper<PZProps>();
const columns = [
columnHelper.accessor("title", {
cell: (info) => (
<span>
<Link
href={`/pzs/${info.row.original.id}`}
>{`Dokument ${info.row.original.id}`}</Link>
</span>
),
header: "Tytuł",
}),
columnHelper.accessor("entry_into_storage_date", {
header: "Data wprowadzenia na stan ",
}),
];
return (
<div>
{isLoading ? (
"loading "
) : (
<TableModel data={PZS?.data} columns={columns} />
)}
</div>
);
};
export default index;
What you're experiencing is due to the Next.js' Automatic Static Optimization.
If getServerSideProps or getInitialProps is present in a page, Next.js
will switch to render the page on-demand, per-request (meaning
Server-Side Rendering).
If the above is not the case, Next.js will statically optimize your
page automatically by prerendering the page to static HTML.
During prerendering, the router's query object will be empty since we
do not have query information to provide during this phase. After
hydration, Next.js will trigger an update to your application to
provide the route parameters in the query object.
Since your page doesn't have getServerSideProps or getInitialProps, Next.js statically optimizes it automatically by prerendering it to static HTML. During this process the query string is an empty object, meaning in the first render router.query.id will be undefined. The query string value is only updated after hydration, triggering another render.
In your case, you can work around this by disabling the query if id is undefined. You can do so by passing the enabled option to the useQuery call.
export const usePZDetailData = (id) => {
return useQuery(["pzs", id], () => fetchPZDetailData(id), {
enabled: id
});
};
This will prevent making the request to the API if id is not defined during first render, and will make the request once its value is known after hydration.

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

How to controling browser back button with react router dom v6?

I've been looking for this question and found it but they're using class components and react router dom v5
What i want is
When user click browser back button I'll redirect them to home page
If you are simply wanting to run a function when a back navigation (POP action) occurs then a possible solution is to create a custom hook for it using the exported NavigationContext.
Example:
import { UNSAFE_NavigationContext } from "react-router-dom";
const useBackListener = (callback) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
const listener = ({ location, action }) => {
console.log("listener", { location, action });
if (action === "POP") {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Usage:
import { useNavigate } from 'react-router-dom';
import { useBackListener } from '../path/to/useBackListener';
...
const navigate = useNavigate();
useBackListener(({ location }) =>
console.log("Navigated Back", { location });
navigate("/", { replace: true });
);
If using the UNSAFE_NavigationContext context is something you'd prefer to avoid then the alternative is to create a custom route that can use a custom history object (i.e. from createBrowserHistory) and use the normal history.listen. See my answer here for details.
Update w/ Typescript
import { useEffect, useContext } from "react";
import { NavigationType, UNSAFE_NavigationContext } from "react-router-dom";
import { History, Update } from "history";
const useBackListener = (callback: (...args: any) => void) => {
const navigator = useContext(UNSAFE_NavigationContext).navigator as History;
useEffect(() => {
const listener = ({ location, action }: Update) => {
console.log("listener", { location, action });
if (action === NavigationType.Pop) {
callback({ location, action });
}
};
const unlisten = navigator.listen(listener);
return unlisten;
}, [callback, navigator]);
};
Well after a long journey to find out how to do that finally i came up with this solution
window.onpopstate = () => {
navigate("/");
}
I came up with a pretty robust solution for this situation, just using browser methods, since react-router-v6's API is pretty sketchy in this department right now.
I push on some fake history identical to the current route (aka a buffer against the back button). Then, I listen for a popstate event (back button event) and fire whatever JS I need, which in my case unmounts the component. If the component unmounts WITHOUT the use of the back button, like by an onscreen button or other logic, we just clean up our fake history using useEffect's callback. Phew. So it looks like:
function closeQuickView() {
closeMe() // do whatever you need to close this component
}
useEffect(() => {
// Add a fake history event so that the back button does nothing if pressed once
window.history.pushState('fake-route', document.title, window.location.href);
addEventListener('popstate', closeQuickView);
// Here is the cleanup when this component unmounts
return () => {
removeEventListener('popstate', closeQuickView);
// If we left without using the back button, aka by using a button on the page, we need to clear out that fake history event
if (window.history.state === 'fake-route') {
window.history.back();
}
};
}, []);
You can go back by using useNavigate hook, that has become with rrd v6
import {useNabigate} from "react-router-dom";
const App = () => {
const navigate = useNavigate();
const goBack = () => navigate(-1);
return (
<div>
...
<button onClick={goBack}>Go back</button>
...
</div>
)
}
export App;
I used <Link to={-1}>go back</Link> and its working in v6, not sure if it's a bug or a feature but seems there is no error in console and can't find any documentation stating this kind of approach
You can try this approach. This worked for me.
import { useNavigate, UNSAFE_NavigationContext } from "react-router-dom";
const navigation = useContext(UNSAFE_NavigationContext).navigator;
const navigate = useNaviagte();
React.useEffect(() => {
let unlisten = navigation.listen((locationListener) => {
if (locationListener.action === "POP") {
//do your stuff on back button click
navigate("/");
}
});
return(() => {
unlisten();
})
}, []);
I'm on rrd#6.8 and testing John's answer worked for me right away for a simple "GO back 1 page", no useNavigate needed:
<Link to={-1}>
<Button size="sm">← Back </Button>
</Link>
So as a simple back button this seems to work without unexpected errors.

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.

Resources