NextJS Zustand persist state - reactjs

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

Related

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

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

How to get browser information for react components in nextjs?

I am writing a react component in nextjs that needs some information about the browser environment before it can be rendered. I know I can get this information from the user agent string after page load with a useEffect like so...
const MyComponent = () => {
const [browser, setBrowser] = useState(null);
useEffect(() => {
const ua = utils.getBrowserFromUA()
setBrowser(ua)
});
if (!browser) {
return <div>Loading...</div>
}
return (
<div>
<SpecialComponent browser={browser} />
</div>
);
};
Is it possible to get these values before the react component renders, before the pageLoad event perhaps? I really only need to calculate these values once and potentially share the values with other components. I can't find anything in the docs apart from running scripts with the beforeInteractive flag but I'm not sure how I would get the data to my component.

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

How to make react router replace component only after the matched route asynchronously loaded..?

So here's the situation - When a Link is clicked the nprogress bar will start and I want react-router to only replace the current component with the matched route once that's done loading asynchronously.. just like in instagram..
But I am only getting this -
Here's my HOC to load component asynchronously --
import React, { useEffect, useState } from "react";
import nprogress from "nprogress";
import "nprogress/nprogress.css";
export default importComponent => props => {
const [loadedComponent, setComponent] = useState(null);
// this works like componentwillMount
if (!nprogress.isStarted()) nprogress.start();
if (loadedComponent) nprogress.done();
useEffect(() => {
let mounted = true;
mounted &&
importComponent().then(
({ default: C }) => mounted && setComponent(<C {...props} />)
);
// componentUnMount
return () => (mounted = false);
}, []);
// return the loaded component
const Component = loadedComponent || <div style={{ flexGrow: 1 }}>..</div>;
return Component;
};
I didn't find a solution to this anywhere on the internet.. so I am asking this question here in stackoverflow. I am hoping someone here can solve this.
Edit: 15-Nov-2021
The old approach doesn't work with routes that uses route params or query such as useLocation, useRouteMatch, etc.
The one I am using now is to use React.lazy, React.Suspense and updating the fallback prop whenever a page is rendered. So when the next page is being loaded, the fallback will be shown which is basically the same component instance as current page. Moreover, with this new approach, you can render the next page whenever you like; after fetching data from backend, or after animation, etc.
Here is a demo and source code.

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