Functional component as initial state value throws children undefined without wrapping in React.memo

699 Views Asked by At

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
2

There are 2 best solutions below

9
Drew Reese On

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>
      )}
    </>
  )
}
0
Darko Tasevski On

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