Bug: Certain uses of render props result in remounts on every render
See original GitHub issueReact 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}
/>
)
// 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
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:5 (3 by maintainers)
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.
You were writing
const Component = props => <BaseComp state={state} setState={setState} {...props} />
in the first example andcreateFrame
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 .