NextJS React Context API not updating value when using useState() - reactjs

I have a React Context API in use in my NextJS web app.
I'd like to persist a value throughout the web app. I know I can use the Consumer tag inside the Provider tag to update the value.
Like so:
<ProfileContext.Provider>
<ProfileContext.Consumer>{... some logic ...}</ProfileContext.Consumer>
</ProfileContext.Provider>
But I'd like to use the useContext() call to update a value by passing in a State object.
user_profile.js
import { useState } from 'react'
import { createContext, useContext } from 'react'
const ProfileContext = createContext()
export function ProfileProvider({ children }) {
const [profile, setProfile] = useState("default value")
return (
<ProfileContext.Provider value={{ profile, setProfile }}>
{children}
</ProfileContext.Provider>
)
}
export const useProfileContext = () => {
return useContext(ProfileContext)
}
The issue is the value I assign using setProfile(...) does not retain value when I navigate to another page.
user_account.js
...
export default function UserAccount() {
const profileContext = useProfileContext()
profileContext.setProfile("SOME TEXT PROFILE VALUE")
console.log("PROFILE CONTEXT: ", profileContext.profile) // This will show previously set value
return (...)
}
_app.js
export default function App(pageProps) {
return (
<ProfileProvider>
<Component {...pageProps} />
</ProfileProvider>
)
}
When I navigate to some other page, the state will not have changed and will return null or the default value.
some_other_page.js
...
export default function Home() {
const profileContext = useProfileContext()
console.log(profileContext.profile) // "default value"
return (...)
}
My main guide was https://www.netlify.com/blog/2020/12/01/using-react-context-for-state-management-in-next.js
Most other examples I've read use the Provier-Consumer tags to access and change the Context value.
Value is not passed through the React Context API

If you want to persist the value through page navigation or refreshes, you'll likely need to store the profile value in something that's persisted and load the value from the data store to the context. You can use something like a db or possibly local storage. On app load, you'll make the API call to retrieve these values and store it in your context.

You should wrap profileContext.setProfile call into useEffect
useEffect(() => {
profileContext.setProfile("SOME TEXT PROFILE VALUE")
}, [profileContext]);
Cannot update a component while rendering a different component warning

Related

NextJS Zustand persist state

I have this github repo: https://github.com/salmanfazal01/next-firebase-starter
Basically I am creating a basic boilerplate with NextJS, Firebase and Zustand as a global state manager
I am unable to persist a few states like theme, user, etc in Zustand. After refreshing the window, it defaults back to the default theme.
I did try the persist middleware provided by zustand and even though that works, it causes content mismatch error from the server and client
Error: Hydration failed because the initial UI does not match what was rendered on the server.
How would I go about persisting the theme mode in the state without using other libraries like next-themes?
Desire:
You want to persist state when moving between different pages while using server side rendering (SSR) with next.js. For example, you want to show a user's profile picture as you move between pages.
Initial implementation:
You save the data into the user's browser's local storage. You are using Zustand to do this.
Problem:
You see an error saying that the client-side content does not match the server rendered html. Example from next.js:
# Unhandled Runtime Error
Error: Text content does not match server-rendered HTML. See more info here: [https://nextjs.org/docs/messages/react-hydration-error](https://nextjs.org/docs/messages/react-hydration-error)
The reason for this is that you are rendering html on the server with a particular variables (For example, user_name). When you then load your page on the client-side and load the user_name variable from your client-side local storage, this variable contains a different value compared to the server-side. This causes a mismatch, which the error message highlights (https://nextjs.org/docs/messages/react-hydration-error).
Solution:
When the client-side html differs, load the variable from the local storage after the component/page has first rendered the html. To solve this with next.js, load your data from local storage only in the useEffect hook (equivalent to onMounted in Nuxt/Vue).
Example with Jotai:
// store.js
import { atomWithStorage } from 'jotai/utils'
export const countAtom = atomWithStorage('countAtom', 0);
// pages/login.js
import {useAtom} from "jotai";
import {countAtom} from "../stores/store";
import { useEffect, useState} from "react";
export default function Login(props) {
const [count, setCount] = useAtom(countAtom); // This gets the data from the store.
const add1 = () => {
setCount((count) => count + 1);
};
const [displayedCount, setDisplayedCount] = useState(); // Set a local variable.
useEffect(() => {
setDisplayedCount(count); // Set the local variable from our store data.
}, [count])
return (
<div >
<Head>
<title>Login</title>
</Head>
<main>
<p>
{ displayedCount }
Hello from the login
<button onClick={add1}>Add 1</button>
</p>
</main>
</div>
)
}
// pages/index.js
import {useAtom} from "jotai";
import {countAtom} from "../stores/store";
import { useEffect, useState} from "react";
export default function Home(props) {
const [count, setCount] = useAtom(countAtom); // This gets the data from the store.
const add1 = () => {
setCount((count) => count + 1);
};
const [displayedCount, setDisplayedCount] = useState(); // Set a local variable.
useEffect(() => {
setDisplayedCount(count); // Set the local variable from our store data.
}, [count])
return (
<div >
<Head>
<title>Home</title>
</Head>
<main>
<p>
{ displayedCount }
Hello from the home page
<button onClick={add1}>Add 1</button>
</p>
</main>
</div>
)
}
I suppose you need to use useEffect() to transfer your Zustand states to useState() to prevent Hydration error.
Your Zustand State Data that you imported in any page / component needs to be updated inside useEffect() to new useState().
https://nextjs.org/docs/messages/react-hydration-error
This error is caused by zustand fetching data from persist middleware storage and updating its store before NextJS hydration complete in the beginning.
This can be simply avoided if you render any zustand store variable after first useEffect hook fired. But if you are using multiple variables inside multiple components then this could be tedious and bad solution.
To avoid it you can create a NextJS Layout and wait till first useEffect hook fire to render child elements.
import { useEffect, useState } from "react"
const CustomLayout = ({ children }) => {
const [isHydrated, setIsHydrated] = useState(false);
//Wait till NextJS rehydration completes
useEffect(() => {
setIsHydrated(true);
}, []);
return (
<>
{isHydrated ? ( <div>{children}</div> ) : <div>Loading...</div>}
</>
);
};
export default CustomLayout;
I ran into similar problem when storing opened state in Zustand for an Advent Calendar site. Each day's state is stored on the client and persisted with Zustand persist. To fix, I just followed the guidance from NextJS to use a Dynamic Import and disable SSR for that component.
import dynamic from 'next/dynamic'
const DynamicHeader = dynamic(() => import('../components/header'), {
ssr: false,
})
Source: NextJS Dynamic Imports
This error is caused by Nextjs SSR so all we have to do is set isSSR to true meaning we meaning we assume on the start our page is being server-side rendered and when its true return null.... so we use useEffect to set isSSR to false on client-side immediately the page gets mounted.
I hope this solves your problem...
import { useState, useEffect } from 'react'
const App = ({ Component, pageProps }: AppProps) => {
const [isSSR, setIsSSR] = useState(true);
useEffect(() => {
setIsSSR(false)
}, [])
if (isSSR) return null;
return (
<Component {...pageProps} />
)
}

How to use mapStateToProps to get a value from the state based on a context value?

I've an application when most of the data are stored in the store but the selected item is provided thought the usage of a React.Context.
React-Redux provide the connect api that accept a mapStateToProps function with state and props as a component.
What I would like, if it didn't break the hooks, is something like:
function mapStateToProps(state){
const selectedItemId = useContext(MySelectedItemContext)
return {
item: state.items[selectedItemId]
}
}
but of course it is not possible since I'm outside of the component and cannot invoke the useContext.
So, I tried to use the old API context:
function mapStateToProps(state, props){
return {
item: state.items[props.id]
}
}
export default connect(mapStateToProps)((props) =>
<MySelectedItemContext.Consumer>
{ selectedItemId => <Component id={selectedItemId} {...props}/> }
</MySelectedItemContext.Consumer>)
but this still not works because the connect returns a new component that has the consumer inside instead of outside and id prop is not defined yet in mapStateToProps.
Any idea?
The best way is to remove mapStateToProps and use useSelector hooks and Redux selectors. But if you need mapStateToProps, then you can wrap your component that must be connected to Redux into another component that will get value from context and will pass it to a component that uses Redux.
// Use this component
export function ContextConsumerComponent() {
const selectedItemId = useContext(SelectedItemIdContext);
return <ReduxConsumerComponent id={selectedItemId} />;
}
function mapStateToProps(state, props) {
return {
item: state.items[props.id]
}
}
const ReduxConsumerComponent = connect(mapStateToProps)((props) => {
// props.item will be here
});

Global state management and Next.js

So, I'm planning on doing a Facebook clone to add to my portfolio, and I want to use, react-Next.js, node.js, express.js, typeORM and postgresSQL ( everything in typescript ), but i have a big issue with global state managment
The Question: So, I was thinking, and I said, ok, I'm going to use redux, i know how to use it and i love it, but, to implement redux in next.js, seems QUITE HARD, so, i was i said, well, what if i don't need to use a global state managment ? what if i just use the SWR hook and revalidate data whenever i create/update data in the fronted ? and that might be ok, but is that a bad idea? shouldn't i do that ?
My Goal : Everything i need to know, is, is it bad if i only use SWR or should in try my best implementing redux i next.js? i have those options, but i just don't know what to do, with create-react-app setting up redux is easy, but in next.js i just don't get it, so, if you can help me with your answer, i would thank you a lot !!
swr+contextApi is used together to replace redux. This is step by step how you can set it up:
create your different hooks. for example
export const createFirstHook =
({ arg1, arg2 }) =>
() => {
// CONDITIONAL USESWR CALL
// isValidation is true whenever you are retrievnig a new data
const { data, isValidating, ...swr } = useSWR(
// write fetching logic
);
return {
...swr,
isValidating,
data,
};
};
in your app, you will have many hooks and combine them like this. Think of this as the main reducer in redux.
import { createFirstHook} from "./useNetwork";
export const setupHooks = (deps) => {
return {
useFirstHook: createFirstHook(deps),
...
};
};
write your context and include hooks in the returned object.
const Web3Context = createContext(null)
const createWeb3State = ({arg1, arg2}) => {
return {
arg1,
arg2,
// Passing hooks here
hooks: setupHooks({erg1,arg2})
}
}
export default function Web3Provider({children}) {
const [web3Api, setWeb3Api] = useState(
// this function will set the hooks
createWeb3State({
arg1: null,
arg2: null,
})
)
// you add more logic
// eventuallay you return this
return (
<Web3Context.Provider value={web3Api}>
{children}
</Web3Context.Provider>
)
}
this is how you reach the provider data in other parts of your app.
import { useContext } from "react";
const context= useContext(Web3Context);
This will make you import import { useContext } from "react" ewerwhere you need the context. Instead, in this provider file, you import the useContext and export this function
export function useWeb3() {
return useContext(Web3Context);
}
in next.js you have to wrap the _app.js with the provider
function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Web3Provider>
<Component {...pageProps} />
</Web3Provider>
</>
);
}
Now you can reach your context and hooks anywhere in your app. In this approach each hook function acts like reducers but good thing is you can use those hooks independently somewhere else in your app.

How can I set a global context in Next.js without getting a "Text content did not match" error?

I have a next.js App that needs to pull SESSION data in to a global context. Currently I have:
// _app.js
const App = ({ Component, pageProps }) => {
const start = typeof window !== 'undefined' ? window.sessionStorage.getItem('start') : 0;
return (
<ElapsedContext.Provider value={start}>
<Component {...pageProps} />
</ElapsedContext.Provider>
)
};
export default App;
and I'm consuming in my Component like so:
function MyComponent(props) {
const start = useContext(ElapsedContext);
return (
// something using start;
);
}
However, when I consume that context in a component, the Component renders on the page as expected, but I get Warning: Text content did not match. Server: "0" Client: "5.883333333333345"
Which I think it because it initially passes the 0, then pulls the number from SESSION storage after the window loads.
How can I fix this warning?
Can I safely ignore this warning?
I've tried using useEffect in the _app.js file (which fires after window is loaded) but the initial state is then not available for my component to build what it needs built on initial render...
Next.js renders pages either during build time or it server renders the page, which means window and therefore sessionStorage is not available as this runs in a Node.js environment.
If you ignore this warning, React will perform a re-render after the page loads. The great thing about server rendering React is that by the time the page loads React doesn't have to perform a re-render, so when this warning shows up you want to avoid it.
Although, because sessionStorage isn't available until the page is rendered in the browser, you'll have to wait to fill your Context until then.
Therefore, one way to avoid this error for your case would be to do something like:
// context.js
export const ElapsedContext = React.createContext(0);
export const ElapsedProvider = ({ children }) => {
const [state, setState] = React.useState(0);
React.useEffect(() => {
// on client side mount, set starting value
setState(window.sessionStorage.getItem('start'))
}, [])
return (
<ElapsedContext.Provider
value={state}
>
{children}
</ElapsedContext.Provider>
);
};
// pages/_app.js
export default function MyApp({ Component, pageProps }) {
return <ElapsedProvider><Component {...pageProps} /></ElapsedProvider>
}
// pages/index.js
export default function MyPage() {
const start = React.useContext(ElapsedContext);
if (start === 0) {
return <Loading />
}
return <MyComponentThatUsesElapsed />
}

React Context API - persist data on page refresh

Let's say we have a context provider set up, along with some initial data property values.
Somewhere along the line, let's say a consumer then modifies those properties.
On page reload, those changes are lost. What is the best way to persist the data so we can retain those data modifications? Any method other than simply local storage?
Yeah, if you want the data to persist across reloads, your options are going to be storing that info server-side (via an api call) or in browser storage (local storage, session storage, cookies). The option you'll want to use depends on what level of persistence you're looking to achieve. Regardless of storage choice, it would likely look something along the lines of
const MyContext = React.createContext(defaultValue);
class Parent extends React.Component {
setValue = (value) => {
this.setState({ value });
}
state = {
setValue: this.setValue,
value: localStorage.getItem("parentValueKey")
}
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
// Whatever storage mechanism you end up deciding to use.
localStorage.setItem("parentValueKey", this.state.value)
}
}
render() {
return (
<MyContext.Provider value={this.state}>
{this.props.children}
</MyContext.Provider>
)
}
}
Context doesn't persist in the way you want. Here's a sample of what I've done, using stateless functional with React hooks.
import React, {useState, useEffect} from 'react'
export function sample(){
// useState React hook
const [data, setData] = useState({})
const [moreData, setMoreData] = useState([])
// useState React hook
useEffect(() => {
setData({test: "sample", user: "some person"})
setMoreData(["test", "string"])
}, [])
return data, moreData
}
export const AppContext = React.createContext()
export const AppProvider = props => (
<AppContext.Provider value={{ ...sample() }}>
{props.children}
</AppContext.Provider>
)
Understand from the start that this isa workaround, not a permanent solution. Persisting data is the job of a database, not the client. However, if you need persisted data for development, this is one way. Notice first that I'm using React hooks. This is a fully supported feature as of 16.8. The useEffect() replaces the lifecycle methods found in class declarations like that of TLadd above. He's using componentDidUpdate to persist. The most up-to-date way of doing this is useEffect. When the app is refreshed this method will be called and set some hard-coded data in context.
To use the provider:
import React from 'react'
import Component from './path/to/component'
import { AppProvider } from './path/to/context'
const App = () => {
return (
<AppProvider>
<Component />
</AppProvider>
)
}
When you refresh, data and moreData will still have whatever default values you assign to them.
I am assuming that you are already familiar with setting context and setting up the context provider.
One of the things you can do is to store the value in the browser's Cookie or any storage available to you, and then, in your Context Provider, retrieve the value, if you can find it, you set it, if not, set the default. If the provider file is a class based component, you would like to retrieve this value in the constructor(), otherwise if it is functional, you can use useLayoutEffect() to set it.

Resources