Looking for an approach that allows me to use state hooks in dynamic lists of functional components. As my code stands a re-rendering of the list gives errors concerning changing numbers of hooks.
I have a master component that renders a list of Functional components, the length of the list depends on the state held by the master, and that state may be updated by an Effect hook; the change happens asynchronously. I want to allow the list items to each have their own state – the state controls some visual effects.
When my list size changes I get errors of this kind:
react-dom.development.js:86 Warning: React has detected a change in the order of Hooks called by EstateOverview. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
- useState useState
- undefined useState
and
Uncaught Error: Rendered more hooks than during the previous render.
The code:
the list, displaying Real Estate Properties as a list of PropertyGroup
import React from 'react'; import List from '@mui/material/List'; import PropertyGroup from "./PropertyGroup.js"; export default function EstateOverview(props) { return ( <List> {props.estateOverview.propertyGroups.map((group) => PropertyGroup(group))} </List> ); }
the list item, this is much simplified, enough just to reproduce the problem
import React from 'react'; export default function PropertyGroup(props) { // trivial state, sufficient to cause error // if I remove this state the demo runs without error // but in my real code I have meaningful state to maintain const [x, setX] = React.useState(false); return ( <h2 key={props.name}>{props.name}</h2> ); }
The above is invoked from this Real Estate master component
import React, { useEffect, useState } from 'react'; import EstateOverview from "./EstateOverview.js"; export default function Estate(props) { const [estateOverview, setEstateOverview] = useState( { "propertyGroups" : [ { name: "The Close"} ] } ); useEffect(() => { const newEstateOverview = { propertyGroups: [ { name: "The Avenue"}, { name: "Broadway" } ] }; // represents new data arriving asynchronously setTimeout(() => { setEstateOverview(newEstateOverview); }, 1000 ); }); return ( <EstateOverview estateName={props.estateName} estateOverview={estateOverview} /> ); }
Advertisement
Answer
The problem is in EstateOverview:
return ( <List> {props.estateOverview.propertyGroups.map((group) => PropertyGroup(group))} </List> );
This is attempting to render a React component by calling it as a function. What you should do, especially when dealing with functional components that contain hooks:
- Use
React.createElement
- Use JSX to render the component (Using JSX calls
React.createElement
under the hood)
This is the correct way to render this:
return ( <List> {props.estateOverview.propertyGroups.map((group) => <PropertyGroup {...group} />)} </List> );