Can I useEffect inside a loop sure to be of constant dimension?

I have a functional React component (more a proxy to a non-React object) like so:

function ProxyComponent({onEvent1, onEvent2, onEvent3 ...}){
  useEffect(()=>{
    someObject?.on('event1', onEvent2);
    return someObject?.off('event1', onEvent2);
  },[onEvent2, someObject])

  useEffect(()=>{
    someObject?.on('event1', onEvent1);
    return someObject?.off('event1', onEvent1);
  },[onEvent1, someObject])
  .
  .
  .
}

Except that there are a lot of events, and there’s an obvious pattern here. So I’d like to do this:
function ProxyComponent(props){
  const events = {
    'event1': props.onEvent1 ?? null,
    ...
    'event20': props.onEvent20 ?? null
  }
  

  for (const [name, handler] of Object.entries(events)) {
    useEffect(()=>{
      handler && someObject?.on(name, handler);
      return handler && someObject?.off(name, handler);
    },[handler, someObject])
  }

}

The rules of hook make it clear that this usage is not supported, but I’d like to know how I can automate this pattern while still being within the rules of hook [This is the main question]

Notes:

  1. The .on and .off methods create network requests so it’s best to call them as little as possible.
  2. Moving the loop inside the useEffect creates two complications:

    a. A very long dependency array [onEvent1, …, onEvent20]. I highly doubt that [...Object.values(events)] works.

    b. Every change in a single handler causes several .offs and undoes it with new .ons again. Apart from the inefficiency, in my particular case, I’d like to avoid this because of note#1.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

I suspect that calling useEffect inside a loop is OK if the number of iterations is absolutely static. The following doesn’t throw an error:

const App = () => {
    for (let i = 0; i < 3; i++) {
      React.useEffect(() => {
        console.log('an effect');
      }, []);
    }
    return 'foo';
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />)
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div class='react'></div>

and the point of the rule is:

By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

which a static loop fulfills. Loops often have conditions that will cause them to execute different numbers of times, which is probably why “Don’t use hooks inside loops” is there – but the blanket statement is not quite as precise as React actually requires.

So, going with your

for (const [name, handler] of Object.entries(events)) {
    useEffect(()=>{

could be fine – the only issue would be with linters.

Setting up an alternative approach is possible, just somewhat convoluted. You need to emulate the behavior of useEffect by comparing the current value of something to a previous value, without using hooks in a loop. One option would be to put the handers (passed by props) into state. Each render, go through the props and check for any inequalities with what’s in state. If there are, resubscribe.

Method 2

You can create a helper component, like the following

function Helper({eventName, eventHandler, someObject}) {
    useEffect(() => {
        someObject?.on(eventName, eventHandler);
        return someObject?.off(eventName, eventHandler);
    }, [eventName, eventHandler, someObject])
}

Then, you can call this component inside your ProxyComponent, inside of a loop:
const events = {
    'event1': props.onEvent1 ?? null,
    ...
    'event20': props.onEvent20 ?? null
}
return (
    <>
        {Object.keys(events).map(event => (
            <Helper eventName={event} eventHandler={events[event]} someObject={someObject} />
        ))}
    </>
)

To prevent calling Helper component on each prop change of the parent, you can export it with React.memo

Method 3

You can do it in a single effect, but you’ll have to memoize and compare dependencies manually:

function ProxyComponent (props) {
  const previousEntriesRef = useRef([]);
  const previousSomeObjectRef = useRef(someObject);

  const eventHandlerMap = {
    'event1': props.onEvent1 ?? null,
    'event2': props.onEvent2 ?? null,
    // etc...
  };

  useEffect(() => {
    const cleanupFns = [];
    const currentEntries = Object.entries(eventHandlerMap);
    const someObjectPrevious = previousSomeObjectRef.current;

    for (const [index, [name, handler]] of currentEntries.entries()) {
      const [namePrevious, handlerPrevious] =
        previousEntriesRef.current[index] ?? [];

      if (
        Object.is(name, namePrevious)
        && Object.is(handler, handlerPrevious)
        && Object.is(someObject, someObjectPrevious)
      ) continue;

      if (!handler) continue;
      someObject?.on(name, handler);
      cleanupFns.push(() => someObject?.off(name, handler));
    }

    previousEntriesRef.current = currentEntries;
    previousSomeObjectRef.current = someObject;

    return () => {
      for (const fn of cleanupFns) fn();
    };
  });
}


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x