1

Trying to set DefaultLayout component as initial state value. It throws cannot de structure property children of undefined.

If I wrap the DefaultLayout component in React.memo it works without any error i.e export default React.memo(DefaultLayout)

Can any one please explain the cause of this behaviour.

Please find sandbox link

https://codesandbox.io/s/autumn-firefly-qp5gh?file=/pages/index.js

Layout.js

import React, { useEffect, useState } from 'react'
import { AppLayout } from 'utilities/appComponentConfig'
import { useGlobalUI } from 'store/GlobalUI'
import { APP_TYPE } from 'utilities/Constants'
import { fetchKioskInfo } from '../services/kiosk'

const Layout = ({ Pages, pageProps, cookie }) => {
  const { setAppType, setAgentCookie } = useGlobalUI()
  const [Component, setComponent] = useState(AppLayout[APP_TYPE.WEB])
  setAgentCookie(cookie)
  useEffect(() => {
    const fetchKiosk = async () => {
      const kioskDetails = await fetchKioskInfo(cookie)
      if (kioskDetails?.length > 0) {
        setAppType(APP_TYPE.KIOSK)
        return setComponent(AppLayout[APP_TYPE.KIOSK])
      } else {
        setAppType(APP_TYPE.WEB)
        return setComponent(AppLayout[APP_TYPE.WEB])
      }
    }

    if(cookie?.includes('KIOSK'))fetchKiosk()
  }, [])

  return (
    <>
      {Component ? (
        <Component pageProps={pageProps}>
          <Pages {...pageProps} />
        </Component>
      ) : null}
    </>
  )
}

export default Layout

appComponentConfig.js

import { APP_TYPE } from './Constants'

import DefaultLayout from 'layouts/DefaultLayout'
import KioskLayout from 'layouts/KioskLayout'

const AppLayout = {
  [APP_TYPE.WEB]: DefaultLayout,
  [APP_TYPE.KIOSK]: KioskLayout,
}

export { AppLayout }

DefaultLayout.js

const DefaultLayout = ({ children, pageProps }) => {
  const mainNode = pageProps.main || {}
  const settings = mainNode.settings || {}
  const showFooter = !settings.hideFooter
  const errorDetail = _find(pageProps, (item) => item.error && item.status)
  const router = useRouter()

  return (
    <>
        <div style={{ display: 'none' }} className="version">
          Version 1.0.5
        </div>
      </div>
    </>
  )
}

export default DefaultLayout

_app.js

import React, { useEffect } from 'react'
import { useCookie } from 'next-cookie'
import '../styles/main.css'
import UserProvider from '../store/User'
import PageProvider from '../store/Page'
import GlobalUIProvider from '../store/GlobalUI'
import BookingProvider from '../store/Booking'
import ExtraProvider from '../store/Extras'
import CartProvider from '../store/Cart'
import CheckoutProvider from '../store/Checkout'
import { useRouter } from 'next/router'
import { storePathValues } from '../utilities/helperFunctions'
import Layout from 'layouts/Layout'

const App = ({ Component: Pages, pageProps, cookie }) => {
  const router = useRouter()

  return (
    <>
      <PageProvider model={pageProps}>
        <GlobalUIProvider>
          <UserProvider>
            <CartProvider>
              <CheckoutProvider>
                <BookingProvider>
                  <ExtraProvider>
                    <Layout
                      Pages={Pages}
                      pageProps={pageProps}
                      cookie={agentCookie}
                    />
                  </ExtraProvider>
                </BookingProvider>
              </CheckoutProvider>
            </CartProvider>
          </UserProvider>
        </GlobalUIProvider>
      </PageProvider>
    </>
  )
}

export default App
Yeshwanth Kumar
  • 81
  • 1
  • 1
  • 8

2 Answers2

0

It may be due to the fact that React.memo is a Higher Order Component, i.e. that it takes a React component as an argument and returns a decorated React component. Perhaps it related to the order in which exports are processed and using the memo HOC tweaks this.

Either way, I think storing React components in state is a bit anti-pattern. I suggest storing the current "active" APP_TYPE value and derive the component to be rendered when this Layout component is rendering.

const Layout = ({ Pages, pageProps, cookie }) => {
  const { setAppType, setAgentCookie } = useGlobalUI();

  const [componentType, setComponentType] = useState(APP_TYPE.WEB);

  setAgentCookie(cookie);

  useEffect(() => {
    const fetchKiosk = async () => {
      const kioskDetails = await fetchKioskInfo(cookie);
      const type = kioskDetails?.length ? APP_TYPE.KIOSK : APP_TYPE.WEB;

      setAppType(type);
      setComponentType(type);
    }

    if (cookie?.includes('KIOSK')) {
     fetchKiosk();
    }
  }, []);

  const Component = AppLayout[componentType];

  return (
    <>
      {Component && (
        <Component pageProps={pageProps}>
          <Pages {...pageProps} />
        </Component>
      )}
    </>
  )
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thank you for the response, I have added the _app.js code where the layout is consumed. Trying to understand what makes the props De-structuring work when DefaultLayout is memoized. Will follow the approach suggested but will be helpful to understand why it errors out without the React.memo – Yeshwanth Kumar Dec 02 '21 at 09:59
  • @YeshwanthKumar Without a codesandbox or cloning your repo and running locally it's difficult to say. It's not immediately clear to me why, ***BUT***... when rendering dynamic runtime components it's more common to compute the component to be rendered *during* the render cycle. – Drew Reese Dec 02 '21 at 10:04
  • Please find the sandbox link https://codesandbox.io/s/autumn-firefly-qp5gh?file=/pages/index.js. – Yeshwanth Kumar Dec 02 '21 at 12:39
  • @YeshwanthKumar Is there something I need to do in that CSB to get it to render anything? I don't see anything rendered nor do I see any logged errors, etc... – Drew Reese Dec 02 '21 at 16:25
  • Extremely sorry, Really appreciate the help just want to understand why it fails. Please check the link now https://codesandbox.io/s/interesting-thompson-psenr. Removed the memoization in the default layout component. – Yeshwanth Kumar Dec 02 '21 at 16:53
  • @YeshwanthKumar Silly me, I forgot that when using Nextjs in CSB you need to actually save the changes in order to rebuild. Testing some things now. I do notice in the stacktrace the error is coming from `Layout` in the `useState` hook though. – Drew Reese Dec 02 '21 at 17:19
  • Yes @Drew, That is the error which we get without wrapping the DefaultLayout in React.memo, If we wrap you can see it works fine. I don't understand what makes the props definition hold good when memoized. – Yeshwanth Kumar Dec 03 '21 at 02:42
  • @YeshwanthKumar I got to tinker a little bit, still looking into this though. – Drew Reese Dec 03 '21 at 03:11
  • Thanks @Drew, I have been trying to wrap this behaviour to my head from weeks. – Yeshwanth Kumar Dec 03 '21 at 10:59
0

This issue is caused by useState behavior; it's a bit sneaky in this case.

useState takes a function as an argument to lazily initialize state and passing the React component triggers that mechanic since the React component is basically a function.

You can fix this in two ways:

(1): Use function to return a function in useState:

const Layout = ({
  Pages,
  pageProps,
  cookie
}) => {
  const { setAppType, setAgentCookie } = useGlobalUI();
  const [Component, setComponent] = useState(() => AppLayout[APP_TYPE.WEB]);

  return (
    <>
      {Component ? (
        <Component pageProps={pageProps}>
          <Pages {...pageProps} />
        </Component>
      ) : null}
    </>
  );
};

(2): Pass APP_TYPE as a prop and resolve the component in useMemo:

const Layout = ({
  Pages,
  pageProps,
  cookie,
  someDynamicAppType = APP_TYPE.WEB`enter code here`
}) => {
  const { setAppType, setAgentCookie } = useGlobalUI();
  const Component = useMemo(() => AppLayout[someDynamicAppType], [
    someDynamicAppType
  ]);

  return (
    <>
      {Component ? (
        <Component pageProps={pageProps}>
          <Pages {...pageProps} />
        </Component>
      ) : null}
    </>
  );
};
Darko Tasevski
  • 396
  • 4
  • 11