skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.


  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search