Home > Mobile >  Trying to dynamically create a new Component, getting error saying Invalid hook call
Trying to dynamically create a new Component, getting error saying Invalid hook call

Time:08-15

I am getting the following error:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

My App and state look like this:

export interface AppState {
    levels: Array<React.ReactElement>;
    builtInLevels: Array<string>;
  };
  
  const initialState: AppState = {
    levels: [],
    builtInLevels: ["yesterday"]
  };
  
  function App() {
  
    const [state, setState] = useState<AppState>(initialState);
 
    const addLevel = (props: LevelProps) => {    
      setState({
        ...state,
        levels: [...state.levels, Level(props)]
      })
    }
  
    const newLevelOnClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      event.preventDefault();
      addLevel({name: "none"});    
    }
  
  
    return (
      <div className="App">
        <div>
            <div><button name="newLevel" value="newLevel" onClick={newLevelOnClick}>new level</button></div>
        </div>

        <div>              
            {state.builtInLevels.map(name =>
                <div key={name} className="m-1"><Level name={name} /></div>
            )}
        </div>
        
        <div>
              {state.levels.map(level =>
                <div className="m-1">{level}</div>
              )}  
        </div>        
        
      </div>
    );
  }
  
  export default App;

When someone clicks the button 'new level', I am appending it to the state and I want the app to re-render the list of components.

CodePudding user response:

Presumably, Level has a hook called somewhere inside of it and when adding it to the state with Level(props), react complains because it is not called as a component.

This could be fixed by adding the Level as a component:

const addLevel = (props: LevelProps) => {    
  setState({
    ...state,
    levels: [...state.levels, <Level {...props} />]
  })
}

But, storing components in the state isnt ideal. It would be better to only store the data needed to construct the components instead:

const addLevel = (props: LevelProps) => {    
  setState({
    ...state,
    levels: [...state.levels, props]
  })
}

and then use the stored level props to render the Levels:

<div>
  {state.levels.map((props, i) =>
    <div className="m-1" key={`${props.name}-${i}`}>
      <Level {...props} />
    </div>
  )}  
</div> 

And then, to remove levels since it was asked:

const removeLevel = (index: number) => {
  const newLevels = [...levels]
  newLevels.splice(index, 1)
  setState({ ...state, levels: newLevels })
}
...
{state.levels.map((props, i) =>
  <div className="m-1" key={`${props.name}-${i}`}>
    <Level {...props} />
    <button onClick={() => removeLevel(i)}>remove</button>
  </div>
)}

However if levels have the same name (theyre all named "none") react will probably not realize it has to rerender levels when you remove one since the keys will not have changed. A better solution would be to have each level have a unique id so that each element in the array will rerender correctly if needed:

interface LevelProps {
  id: number | string;
  name: string;
}
const removeLevel = (id: number | string) => {
  const newLevels = levels.filter(level => level.id !== id)
  setState({ ...state, levels: newLevels })
}
...
{state.levels.map((props) =>
  <div className="m-1" key={props.id}>
    <Level {...props} />
    <button onClick={() => removeLevel(props.id)}>remove</button>
  </div>
)}
  • Related