Home > Software engineering >  "Rendered fewer hooks than expected", but there are no direct hooks
"Rendered fewer hooks than expected", but there are no direct hooks

Time:06-06

I get this error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

In itself, this is nothing new to me and I know how to fix this, but I can't figure it out in this case.

import reactStringReplace from 'react-string-replace';

import { Entity, Player } from './LogItem';

export default function PrepareText({
  subjects,
  text,
  EntityPill,
  PlayerPill,
}: {
  subjects: { player: Player; entity: Entity };
  text: string;
  EntityPill: (text: string | null) => React.ReactNode;
  PlayerPill: (text: string | null) => React.ReactNode;
}) {
  return (
    <>
      {reactStringReplace(text, /(\${\w })/g, (match, i) => {
        const key = match
          .replace('${', '')
          .replace('}', '') as keyof typeof subjects;

        return (
          <span key={i}>
            {key === 'player'
              ? PlayerPill(subjects[key])
              : EntityPill(subjects[key])}
          </span>
        );
      })}
    </>
  );
}

I think the problem is with conditionally rendering the two components PlayerPill and EntityPill because these use hooks inside. But usually, it's not e problem to conditionally render components.

Is it because I call them as functions? Is there e different way to pass props to a React.ReactNode?

If there is a better option to do this I would be very excited to implement it.

CodePudding user response:

Sorry, I'm confused: why don't you simply do this:

return (
   <span key={i}>
      {key === 'player'
          ? <PlayerPill text={subjects[key]}/>
          : <EntityPill text={subjects[key]}/>
      }
   </span>
);

CodePudding user response:

Is it because I call them as functions?

Yes. You're not using them as components, you're using them as sub-functions of PrepareText. That means they use the component context of PrepareText, not their own context. So hooks save information to the underlying PrepareText instance, not their own, and so if the number of times you call them varies from render to render, it won't work correctly.

Instead, make them actual components and pass them information as props, not arguments; or make them render components using the arguments you pass them.

Here's an example of passing component functions (CompA/CompB, passed as Sub1/Sub2) to another component (Example) that uses them conditionally based on a 50/50 coin flip:

const { useState, useEffect } = React;

const flipCoin = () => Math.random() < 0.5;

const CompA = ({text}) => {
    const [counter, setCounter] = useState(0);
    useEffect(() => {
        const timer = setInterval(() => {
            setCounter(c => c   1);
        }, 800);
    }, []);
    return <div>CompA, {text}, counter = {counter}</div>;
};

const CompB = ({text}) => {
    const [counter, setCounter] = useState(0);
    useEffect(() => {
        const timer = setInterval(() => {
            setCounter(c => c   1);
        }, 800);
    }, []);
    return <div>CompB, {text}, counter = {counter}</div>;
};

const Example = ({Sub1, Sub2}) => {
    const [counter, setCounter] = useState(0);
    return <div>
        <input type="button" value="Re-render" onClick={() => setCounter(c => c   1)} />
        {flipCoin() && <Sub1 text="a" />}
        {flipCoin() && <Sub2 text="b" />}
    </div>;
};

const App = () => {
    return <Example Sub1={CompA} Sub2={CompB} />;
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

  • Related