React-hot-loader: Cannot read property 'propTypes' of undefined at updateMemoComponent

Created on 19 Apr 2019  路  6Comments  路  Source: gaearon/react-hot-loader

Description

Started seeing this one last night, I need to try and isolate it a bit more.

Actual behavior

Getting this error. Specifically, it seems to have after the second save on a certain page. Never the first save, and other pages are working normally.

Uncaught TypeError: Cannot read property 'propTypes' of undefined
    at updateMemoComponent (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:14308)
    at beginWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:15486)
    at performUnitOfWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:19109)
    at workLoop (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:19149)
    at renderRoot (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:19232)
    at performWorkOnRoot (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:20139)
    at performWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:20051)
    at performSyncWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:20025)
    at requestWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:19894)
    at scheduleWork (webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:19708)
webpack-internal:///../../node_modules/@hot-loader/react-dom/cjs/react-dom.development.js:16914 The above error occurred in the <InnerNotFoundBoundary> component:
    in InnerNotFoundBoundary (created by Context.Consumer)
    in ErrorBoundary (created by Layout)
    in div (created by ForwardRef(Gloss))
    in ForwardRef(Gloss)
    in ForwardRef(Gloss) (created by Layout)
    in SimpleProvider (created by Layout)
    in ThemeByName (created by Theme)
    in Theme (created by Layout)
    in ThemeProvide
    in Provider
    in Provider
    in Provider
    in Provider
    in Unknown (created by Layout)
    in Layout
    in NaviProvider (created by Router)
    in Router
    in Unknown (created by SiteRoot)
    in AppContainer (created by SiteRoot)
    in SiteRoot

Environment

React Hot Loader version: 4.8.4

Have the @hot-loader dom patch, and the following settings:

setConfig({
    logLevel: 'no-errors-please',
    pureSFC: true,
    pureRender: true,
    // disableHotRenderer: true,
  })

The page that it's running on is a bit large, I'll just paste it here anyway:

import {
  BorderRight,
  Button,
  Col,
  gloss,
  Input,
  List,
  Portal,
  RoundButton,
  Row,
  Section,
  Sidebar,
  SpaceGroup,
  SubTitle,
  SurfacePassProps,
  Toolbar,
  useMedia,
} from '@o/ui'
import { compose, mount, route, withView } from 'navi'
import React, { memo, useEffect, useRef, useState } from 'react'
import { useNavigation, View } from 'react-navi'
import { useScreenSize } from '../hooks/useScreenSize'
import { useSiteStore } from '../Layout'
import { Header } from '../views/Header'
import { MDX } from '../views/MDX'
import { SectionContent } from '../views/SectionContent'
import DocsInstall from './DocsPage/DocsInstall.mdx'

const views = {
  install: () => import('./DocsPage/DocsInstall.mdx'),
  buttons: () => import('./DocsPage/DocsButtons.mdx'),
  cards: () => import('./DocsPage/DocsCards.mdx'),
  progress: () => import('./DocsPage/DocsProgress.mdx'),
  lists: () => import('./DocsPage/DocsLists.mdx'),
  start: () => import('./DocsPage/DocsStart.mdx'),
}

export default compose(
  withView(req => {
    const id = req.path.slice(1)
    return (
      <DocsPage id={id}>
        <View />
      </DocsPage>
    )
  }),

  mount({
    '/': route({
      title: 'Docs',
      view: <DocsInstall />,
    }),
    '/:id': route(async req => {
      let id = req.params.id
      if (!views[id]) {
        return {
          view: () => <div>not found</div>,
        }
      }
      let ChildView = (await views[id]()).default || (() => <div>nada {id}</div>)
      return {
        view: <ChildView />,
      }
    }),
  }),
)

function DocsPage(props: { id?: string; children?: any }) {
  const screen = useScreenSize()
  const itemIndex = categories.all.findIndex(x => x['id'] === props.id) || 1
  const item = categories.all[itemIndex]
  const siteStore = useSiteStore()
  const [showSidebar, setShowSidebar] = useState(true)
  const [section, setSection] = useState('all')
  const toggleSection = val => setSection(section === val ? 'all' : val)
  const nav = useNavigation()
  const [search, setSearch] = useState('')
  const inputRef = useRef(null)

  const content = (
    <React.Fragment key="content">
      <DocsToolbar section={section} toggleSection={toggleSection} />
      <List
        search={search}
        selectable
        alwaysSelected
        defaultSelected={itemIndex}
        items={categories[section]}
        onSelect={rows => {
          nav.navigate(`/docs/${rows[0].id}`, { replace: true })
        }}
      />
    </React.Fragment>
  )

  const isSmall = screen === 'small'

  useEffect(() => {
    const keyPress = e => {
      // console.log('e', e.keyCode)
      switch (e.keyCode) {
        case 84: // t
          inputRef.current.focus()
          break
      }
    }
    window.addEventListener('keydown', keyPress)
    return () => {
      window.removeEventListener('keydown', keyPress)
    }
  }, [])

  useEffect(() => {
    inputRef.current && inputRef.current.focus()
  }, [inputRef.current])

  return (
    <MDX>
      <Portal prepend style={{ position: 'sticky', top: 10, zIndex: 10000000 }}>
        <Row margin={[0, 'auto']} pointerEvents="auto" pad={['sm', 100]} width="90%" maxWidth={800}>
          <Input
            ref={inputRef}
            onChange={e => setSearch(e.target.value)}
            flex={1}
            sizeRadius={10}
            size="xl"
            iconSize={16}
            icon="search"
            placeholder="Search the docs..."
            elevation={5}
            after={
              <Button tooltip="Shortcut: t" size="sm" alt="flat" fontWeight={600}>
                t
              </Button>
            }
          />
        </Row>
      </Portal>
      <Portal prepend>
        <Header slim />
      </Portal>
      <Portal>
        <FixedLayout>
          {isSmall ? (
            <Sidebar
              hidden={!showSidebar}
              zIndex={10000000}
              elevation={5}
              pointerEvents="auto"
              // @ts-ignore
              background={theme => theme.background}
            >
              {content}
            </Sidebar>
          ) : (
            <SectionContent pointerEvents="none" flex={1}>
              <Col position="relative" flex={1} width={300} pointerEvents="auto">
                {content}
                <BorderRight opacity={0.5} />
              </Col>
            </SectionContent>
          )}
        </FixedLayout>
      </Portal>

      <SectionContent fontSize={16} lineHeight={28}>
        <ContentPosition isSmall={isSmall}>
          <DocsContents
            onToggleSidebar={() => setShowSidebar(!showSidebar)}
            setTheme={siteStore.setTheme}
            theme={siteStore.theme}
            title={item ? item['title'] : undefined}
          >
            {props.children}
          </DocsContents>
        </ContentPosition>
      </SectionContent>
    </MDX>
  )
}

DocsPage.theme = 'light'

const ContentPosition = gloss<{ isSmall?: boolean }>({
  width: '100%',
  padding: [0, 0, 0, 300],
  isSmall: {
    padding: [0, 0, 0, 0],
  },
})

const FixedLayout = gloss({
  position: 'fixed',
  top: 100,
  left: 0,
  width: '100%',
  height: '100%',
  zIndex: 100000,
})

const DocsToolbar = memo(({ section, toggleSection }: any) => {
  return (
    <Toolbar background="transparent" pad="xs" justifyContent="center" border={false}>
      <RoundButton
        alt={section === 'docs' ? 'selected' : 'flat'}
        onClick={() => toggleSection('docs')}
      >
        Docs
      </RoundButton>
      <RoundButton alt={section === 'ui' ? 'selected' : 'flat'} onClick={() => toggleSection('ui')}>
        UI
      </RoundButton>
      <RoundButton
        alt={section === 'kit' ? 'selected' : 'flat'}
        onClick={() => toggleSection('kit')}
      >
        Kit
      </RoundButton>
    </Toolbar>
  )
})

const DocsContents = memo(({ setTheme, theme, title, onToggleSidebar, children }: any) => {
  const isSmall = useMedia({ maxWidth: 700 })
  return (
    <Section
      maxWidth={600}
      margin={[0, 'auto']}
      pad={['xl', 'xl', true, 'xl']}
      titleBorder
      space
      title={title || 'No title'}
      titleSize="xl"
      afterTitle={
        <SurfacePassProps iconSize={12}>
          <SpaceGroup space="xs">
            <Button
              icon="moon"
              tooltip="Toggle dark mode"
              onClick={() => setTheme(theme === 'home' ? 'light' : 'home')}
            />
            {isSmall && (
              <Button icon="panel-stats" tooltip="Toggle menu" onClick={onToggleSidebar} />
            )}
          </SpaceGroup>
        </SurfacePassProps>
      }
    >
      {children}
    </Section>
  )
})

const titleItem = { titleProps: { size: 1.1 } }

const ListSubTitle = gloss(SubTitle, {
  margin: [20, 0, -2],
  fontWeight: 300,
  fontSize: 18,
})

const docsItems = [
  {
    selectable: false,
    hideBorder: true,
    children: <ListSubTitle>Start</ListSubTitle>,
  },
  {
    id: 'start',
    title: 'Getting started',
    ...titleItem,
  },
]

const uiItems = [
  {
    selectable: false,
    hideBorder: true,
    children: <ListSubTitle>User Interface</ListSubTitle>,
  },

  { id: 'lists', title: 'Lists', icon: 'th-list', group: 'Collections' },
  { id: 'tables', title: 'Tables', icon: 'th' },
  { id: 'tree', title: 'Tree', icon: 'diagram-tree' },
  { id: 'treeList', title: 'TreeList', icon: 'chevron-right' },
  { id: 'definitionList', title: 'DefinitionList', icon: 'list-columns' },

  {
    group: 'Views',
    id: 'surfaces',
    icon: 'layer',
    title: 'Surface',
    subTitle: 'Building block of many views',
  },
  { id: 'icons', icon: 'star', title: 'Icons', indent: 1 },
  { id: 'buttons', icon: 'button', title: 'Buttons', indent: 1 },
  { id: 'cards', title: 'Cards', icon: 'credit-card', indent: 1 },
  { id: 'install', title: 'Sections', icon: 'application' },
  { id: 'install', title: 'Popovers', icon: 'direction-right' },
  { id: 'install', title: 'Decorations', icon: 'clean' },
  { id: 'progress', title: 'Progress', icon: 'circle' },
]

const categories = {
  all: [...docsItems, ...uiItems],
  ui: uiItems,
  docs: docsItems,
  kit: uiItems,
}

Most helpful comment

Probably I've found a problem. There are two different "memo" components - a MemoComponent, and SimpleMemoComponent.

RHL works for the second one, breaks for the first. I don't get yet, where which one is used(and how to create a test for it), but it's clear how I am breaking it

All 6 comments

Seems like what's tripping it up is the <ListSubTitle>Start</ListSubTitle>, that view is a a memo(forwardRef()) component, possible the usage of it inline in the file.

Sorry - there is no way I could reproduce the problem, but you are trying to memo(undefined)

I'm facing same error when using react.memo!

@theKashey

but you are trying to memo(undefined)

Not exactly... debugger says:

updateMemoComponent(/*...*/) {
    // ...
    var _type = Component.type; // <-- Component is a function and has no type!
    var _innerPropTypes = _type.propTypes; // <-- _type is undefined
    if (_innerPropTypes) {
      // Inner memo component props aren't currently validated in createElement.
      // We could move it there, but we'd still need this for lazy code path.
      checkPropTypes(_innerPropTypes, nextProps, // Resolved props
      'prop', getComponentName(_type), getCurrentFiberStackInDev);
    }
}

For some components it works, but for others - fails. Maybe it depends when memoized component is being used directly with some other HOC.

In my case: WithContext is a HOC, WorstInstruments is a memoized component

image

And I'm always getting hot-update error.

// withContext

 *
 * @param {React.Context<any>} Context
 * @param [opts = {}]
 *  @param [opts.mergeProps]
 * @returns {function(React.ComponentType<any>): React.ComponentType<any>}
 */
const withContext = (Context, opts = {}) => WrappedComponent => {
  const { mergeProps = defaultMergeProps } = opts;

  function WithContext(props) {
    return (
      <Context.Consumer>
        {ctx => {
          const propsToWrapped = mergeProps(ctx, props);

          return <WrappedComponent {...propsToWrapped} />;
        }}
      </Context.Consumer>
    );
  }

  return hoistNonReactStatics(WithContext, WrappedComponent);
};

Probably I've found a problem. There are two different "memo" components - a MemoComponent, and SimpleMemoComponent.

RHL works for the second one, breaks for the first. I don't get yet, where which one is used(and how to create a test for it), but it's clear how I am breaking it

This issue should be solved simultaneously with #1135, ie since v4.6.3.

I am trying to write a failing test, or at least some example to play with, not it always works :(

PS: testing with react-dom 16.8.6

v4.8.5 should fix this

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Anahkiasen picture Anahkiasen  路  5Comments

ghost picture ghost  路  3Comments

lemonmade picture lemonmade  路  3Comments

reintroducing picture reintroducing  路  4Comments

zlk89 picture zlk89  路  3Comments