I’m just starting out with NextJS. I found in the docs that the old method to persist components/state between renders was to use _app.js
, but that is now deprecated in NextJS 13. The new routing model lets you have a layout.js
file where you can put all the common components. My problem is that I want to use a Context
component in the layout file. But the layout file keeps getting rerendered on page change, so the state in the Context gets reset. I know that you can persist state by storing it in localstorage, but if possible, I’d like a way where you don’t need to store it in localstorage, to save resources.
Here is the code for the root layout:
"use client"
import './globals.css'
import 'react-loading-skeleton/dist/skeleton.css'
import localFont from 'next/font/local'
import { Analytics } from '@vercel/analytics/react'
import { TailwindIndicator } from '#/ui/TailwindIndicator'
import Script from 'next/script'
//State
import AudioPlayerContext from '#/state/AudioPlayerContext';
import { useState, useRef, useEffect, useMemo } from 'react'
import AudioPlayerState from '#/lib/types/AudioPlayerState'
import MusicPlayer from '#/ui/MusicPlayer'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const initial_audio_state = useMemo( (): AudioPlayerState => {
const audio_state_str = localStorage.getItem( 'audioState');
if( audio_state_str ) {
return JSON.parse( audio_state_str );
} else {
return {
isPlaying: false,
currentAudio: null
}
}
}, [] );
const [ audioState, setAudioState ] = useState< AudioPlayerState >( initial_audio_state );
const audioObj = useRef( new Audio( undefined ) );
useEffect( () => {
console.log(' audio state changed, updating localstorage ', audioState );
localStorage.setItem('audioState', JSON.stringify( audioState ) );
}, [ audioState ] );
return (
<html
lang="en"
className={`${mono.variable} ${fontHeadings.variable} ${fontHeavy.variable} ${fontBody.variable} ${fontMedium.variable} ${fontBold.variable}`}
>
<Script src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`} />
<Script id="google-analytics">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}');
`}
</Script>
<body>
<AudioPlayerContext.Provider value={ { audioState: audioState, setAudioState: setAudioState, audioObj: audioObj } }>
<MusicPlayer />
{children}
<Analytics />
<TailwindIndicator />
</AudioPlayerContext.Provider>
AAAAA
</body>
</html>
)
}
Ideally I’d like for the AudioPlayerContext.Provider
not to rerender on each route change, as it currently does.
Thanks.
UPDATED CODE:
import './globals.css'
import 'react-loading-skeleton/dist/skeleton.css'
import localFont from 'next/font/local'
import { Analytics } from '@vercel/analytics/react'
import { TailwindIndicator } from '#/ui/TailwindIndicator'
import Script from 'next/script'
const { GA_MEASUREMENT_ID } = process.env
//State
import MusicPlayer from '#/ui/MusicPlayer'
import AppWrapper from './AppWrapper'
const fontHeadings = localFont({
src: '../fonts/gravity.woff2',
display: 'swap',
variable: '--font-headings',
})
const fontBody = localFont({
src: '../fonts/matter-sq-regular.ttf',
display: 'swap',
variable: '--font-body',
})
const fontMedium = localFont({
src: '../fonts/Matter-Medium.ttf',
display: 'swap',
variable: '--font-medium',
})
const fontBold = localFont({
src: '../fonts/Matter-Bold.ttf',
display: 'swap',
variable: '--font-bold',
})
const fontHeavy = localFont({
src: '../fonts/Matter-Heavy.ttf',
display: 'swap',
variable: '--font-heavy',
})
const mono = localFont({
src: '../fonts/RobotoMono-Medium.ttf',
display: 'swap',
variable: '--font-mono',
})
// TODO: See: https://beta.nextjs.org/docs/api-reference/metadata
// export const metadata = {
// title: 'Fankee | Web3 music label',
// description: 'Transform the music industry with Fankee and the blockchain',
// keywords: 'TODO: Define keywords',
// themeColor: '#1d1e1b',
// }
export default function RootLayout({ children }: { children: React.ReactNode }) {
console.log(' rerender ')
return (
<html
lang="en"
className={`${mono.variable} ${fontHeadings.variable} ${fontHeavy.variable} ${fontBody.variable} ${fontMedium.variable} ${fontBold.variable}`}
>
<Script src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`} />
<Script id="google-analytics">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}');
`}
</Script>
<body>
<AppWrapper>
<MusicPlayer />
{children}
<Analytics />
<TailwindIndicator />
</AppWrapper>
</body>
</html>
)
}
'use client'
import AudioPlayerContext from '#/state/AudioPlayerContext'
import { useState, useRef, useEffect, useMemo, ReactNode } from 'react'
import AudioPlayerState from '#/lib/types/AudioPlayerState'
export default function AppWrapper({ children }: { children: ReactNode }) {
const initial_audio_state = useMemo((): AudioPlayerState => {
let audio_state_str = null
if (typeof window !== 'undefined') {
audio_state_str = localStorage.getItem('audioState')
}
if (audio_state_str) {
return JSON.parse(audio_state_str)
} else {
return {
isPlaying: false,
currentAudio: null,
}
}
}, [])
const [audioState, setAudioState] = useState<AudioPlayerState>(initial_audio_state)
//const audio = Audio ? new Audio(undefined) : null
const audioObj = useRef(null as any)
useEffect(() => {
audioObj.current = new Audio(undefined)
console.log(' mounted layout component ')
// if( audioState.isPlaying && audioState.currentAudio ) {
// audioObj.current.src = audioState.cu
// }
}, [])
useEffect(() => {
console.log(' audio state changed, updating localstorage ', audioState)
if (typeof window !== 'undefined') {
localStorage.setItem('audioState', JSON.stringify(audioState))
}
}, [audioState])
return (
<AudioPlayerContext.Provider
value={{
audioState: audioState,
setAudioState: setAudioState,
audioObj: audioObj,
}}
>
{children}
</AudioPlayerContext.Provider>
);
};
2
Answers
UPDATE
So it turns out that the state does persist if you navigate to a different page from within the website, for example by clicking on a link. But the state doesn't persist if you refresh the page or navigate to a page by typing a url in the url bar. I don't think there's any way to solve this without using localstorage.
The root layout cannot be set to a client componant.
As you can see here (last point)
I advise you to change your code accordingly, putting your app as a client componant inside your server side root layout.