React: Bug: Certain uses of render props result in remounts on every render

Created on 3 Mar 2020  路  4Comments  路  Source: facebook/react

React version: 16.13

Both of the following code snippets produce an input component that loses focus on render:
Sandbox

// render prop is passed a component it can render
const Wizard = ({ components, render }) => {
  const [state, setState] = React.useState({})
  const BaseComp = components[0]
  const Component = props => <BaseComp state={state} setState={setState} {...props} />
  return render({ Component })
}

const ChildComponent = ({ state, setState }) => {
  React.useEffect(() => console.log("mounting"), []) // for demonstration purposes only
  const { foo = "" } = state
  const handleChange = ({ target: { name, value } }) => {
    setState(prevState => ({ ...prevState, [name]: value }))
  }
  return <input name="foo" value={foo} onChange={handleChange} />
}

const Frame = ({ Component }) => (
  <div style={{ margin: 20 }}>
    <Component /> // {Component()} works
  </div>
)

const App = () => (
  <Wizard
    components={[ChildComponent]}
    render={Frame}
  />
)

Sandbox

// render prop is passed already rendered children
const Wizard = ({ components, render }) => {
  const [state, setState] = React.useState({})
  const BaseComp = components[0]
  const children = <BaseComp state={state} setState={setState} />
  return render({ children })
}

const ChildComponent = ({ state, setState }) => {
  React.useEffect(() => console.log("mounting"), []) // for demonstration purposes only
  const { foo = "" } = state
  const handleChange = ({ target: { name, value } }) => {
    setState(prevState => ({ ...prevState, [name]: value }))
  }
  return <input name="foo" value={foo} onChange={handleChange} />
}

const createFrame = () => ({ children }) => (
  <div style={{ margin: 20 }}>{children}</div>
)

const Frame = createFrame()

const App = () => (
  <Wizard
    components={[ChildComponent]}
    render={props => {
      const Comp = createFrame()
      // const Comp = Frame // uncomment this line and comment out the one above and it works
      return <Comp {...props} /> // return createFrame()(props) also works
    }}
  />
)

Context

In an app I'm working on there is a wizard component that does state management for an array of pages. It takes a render prop which is passed the current page and other stuff like status and navigation functions so that the look of the wizard is separated from the state management. As the render props passed to different wizards have gotten more complex, we've had issues with inputs losing focus and even some app crashes.

Preliminary Investigation

In both of the above examples, ChildComponent is being remounted on every render of the wizard.

One thing I noticed in the 2nd example is that it can be fixed by swapping Frame and createFrame(). I find this concerning because I thought functional components were pure functions and when working with pure functions you can swap the left side of an assignment for the right side without affecting the result.

Edit:
It seems to be partially a syntax issue. Both examples can be made to work by avoiding JSX

Unconfirmed

Most helpful comment

You were writing const Component = props => <BaseComp state={state} setState={setState} {...props} /> in the first example and createFrame in the second. So in both examples, a new type of component is generated on every render. This a hint that React should unmount the old component and mount the new one instead of reusing the old one .

All 4 comments

You were writing const Component = props => <BaseComp state={state} setState={setState} {...props} /> in the first example and createFrame in the second. So in both examples, a new type of component is generated on every render. This a hint that React should unmount the old component and mount the new one instead of reusing the old one .

@jddxf So why does avoiding JSX seem to solve the problem?
In the first example, why does {Component()} work when <Component /> doesn't?
In the second example, why does createFrame()(props) work?

The non-jsx equivalent of <Component /> is React.createElement(Component) rather than {Component()}. Only things passed to React.createElement as the first parameter will be regarded as component types by React.

This should explain what's happening: https://reactjs.org/docs/reconciliation.html#elements-of-different-types

In general, defining components inside other components (or Hooks) is a mistake. Instead, usually you want to declare them outside.

Was this page helpful?
0 / 5 - 0 ratings