IvanD
IvanD

Reputation: 2953

Rendered more hooks than during the previous render when using hooks in subcomponent

I am trying to understand how I can use React Hooks in subcomponents without triggering an error.

Let's say I have a super simple component (all the code is runnable here - https://codesandbox.io/embed/xenodochial-wright-mwqlk?fontsize=14&hidenavigation=1&theme=dark):

import { useEffect, useState } from "react";

const TestComponent = (index) => {
  const [a, setA] = useState("nothing");

  useEffect(() => {
    setA(index);
  }, [setA, index]);

  return <>{a}</>;
};

export default TestComponent;

And it is used like this:

import { useEffect, useState } from "react";
import Component from "./Component";

export default function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    setArr([1]);
  }, []);

  const component = arr.map((_, index) => Component(index));

  return <div>{component}</div>;
}

Now I am getting an error:

Error
Rendered more hooks than during the previous render.
▶ 5 stack frames were collapsed.
TestComponent
/src/Component.js:4:29
  1 | import { useEffect, useState } from "react";
  2 | 
  3 | const TestComponent = (index) => {
> 4 |   const [a, setA] = useState("nothing");
    |                             ^
  5 | 
  6 |   useEffect(() => {
  7 |     setA(index);

There are more details in the console:

Warning: React has detected a change in the order of Hooks called by App. 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
   ------------------------------------------------------
1. useState                   useState
2. useEffect                  useEffect
3. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    at App (https://mwqlk.csb.app/src/App.js:24:41)

I understand the basic concept of the hooks rules that I cannot use hooks after if statements or such, they should always be on the top level.

However, this case is clearly not using any conditionals for declaring the hooks. All additional hooks are actually in a subcomponent. So why are they accounted for by React as part of the App component?

So, how can I safely use hooks in imported subcomponents then?

Upvotes: 0

Views: 398

Answers (3)

Evren
Evren

Reputation: 4410

You should refactor in this way

Component.js

import { useEffect, useState } from "react";

const TestComponent = ({index}) => {
  const [a, setA] = useState("nothing");

  useEffect(() => {
    setA(index);
  }, [setA, index]);

  return <>{a}</>;
};

export default TestComponent;

App.js

import { useEffect, useState } from "react";
import TestComponent from "./Component";

export default function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    setArr([1]);
  }, []);

  const component = arr.map((_, index) => <TestComponent index={index}/>);

  return <div>{component}</div>;
}

Upvotes: 1

Sean
Sean

Reputation: 1456

Here's how you should pass

App.js:

import { useEffect, useState } from "react";
import Component from "./Component";

export default function App() {
  const [arr, setArr] = useState([]);

  useEffect(() => {
    setArr([10,12,12,12,12]);
  }, []);

  return <>{arr.map((_, index) => (
    <Component key={index} index={index}/>
  ))}</>
}

Test component:

import { useEffect, useState } from "react";

const TestComponent = ({index}) => {
  const [a, setA] = useState("");

  useEffect(() => {
    setA(index);
  }, []);

  return <>{a}</>;
};

export default TestComponent;

Upvotes: 1

larz
larz

Reputation: 5766

You're calling your react component like a function instead of using it as a component. This causes some weirdness with rendering. If you change your .map line to

const component = arr.map((_, index) => <Component index={index} />);

and destructure your props appropriately in Component

const TestComponent = ({ index }) => {

everything works as expected.

Upvotes: 2

Related Questions