title: React hooks: what is best practice for wrapping multiple instances of a hook with a single hook?
body: <p>I have a react hook <code>useDbReadTable</code> for reading data from a database that accepts initial data of <code>tablename</code> and <code>query</code>.  It returns an object that includes an <code>isLoading</code> status in addition to the data from the database.</p>
  <p>I want to wrap this hook in a new hook that accepts initial data of an <strong>array</strong> of <code>{ tablename, query }</code>, and returns an object with the data from the database for each table, but with the <code>isLoading</code> statuses consolidated into a single boolean based on logic in my new hook.</p>
  <p>The idea is, the caller of the new hook can ask for data from a number of tables, but only has to check one status value.</p>
  <p>My thought was to have the new hook look something like,</p>
  <p><strong>EDIT:</strong>  Updated code (I had pasted the wrong version)</p>
  <pre><code>export const useDbRead = tableReads =&gt; {
    let myState = {};
  
    for (let i = 0; i &lt; tableReads.length; ++i) {
      const { tablename, query = {} } = tableReads[i];
      const [{ isLoading, isDbError, dbError, data }] = useDbReadTable(tablename, query);
      myState = { ...myState, [tablename]: { isLoading, isDbError, dbError, data }};
    }
  
    const finalState = {
      ...myState,
      isLoading: Object.values(myState).reduce((acc, t) =&gt; acc || t.isLoading, false),
    };
  
    return [finalState];
  };
  </code></pre>
  <p>However, eslint gives me this error on my <code>useDbReadTable</code> call:</p>
  <blockquote>
  <p>React Hook &quot;useDbReadTable&quot; may be executed more than once. Possibly because it is called in a loop. React Hooks must be called in the exact same order in every component render.  react-hooks/rules-of-hooks</p>
  </blockquote>
  <p>And <a href="https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level" rel="nofollow noreferrer">Rules for Hooks</a> says,</p>
  <blockquote>
  <p>Only Call Hooks at the Top Level</p>
  <p>Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. 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. (If you’re curious, we’ll explain this in depth below.)</p>
  </blockquote>
  <p>After reading the rule and the explanation, it seems the only issue is making sure the hooks are called in the same order on all re-renders.  As long as I ensure the list of tables I pass in to my new hook never changes, shouldn't my new hook work fine (as my initial tests indicate)?  Or am I missing something?</p>
  <p>More importantly, is there a better idea how to implement this, that doesn't violate the Rules of Hooks?</p>
  <p><strong>Edit2:</strong> in case its helpful, here's <code>useDbReadTable</code>.  Note that it includes more functionality than I mention in my question, since I wanted to keep the question as simple as possible.  My question is whether my <code>useDbRead</code> is a good solution, or is there a good way to do it without violating the Rules of Hooks?</p>
  <pre><code>export const useDbReadTable = (initialTableName, initialQuery = {}, initialData = []) =&gt; {
    const dbChangeFlag = useSelector(({appState}) =&gt; appState.dbChangeFlag);
    const [tableName, setTableName] = useState(initialTableName);
    const [query, setQuery] = useState(initialQuery);
    const [state, dispatch] = useReducer(dataFetchReducer, {
      isLoading: false,
      isDbError: false,
      dbError: {},
      data: initialData,
    });
    useEffect(() =&gt; {
      let didCancel = false;
  
      const fetchData = async () =&gt; {
        dispatch({ type: dataFetch.FETCH_INIT });
  
        try {
  
          const result = Array.isArray(query) ?
            await db[tableName].batchGet(query)  // query is an array of Ids
            :
            await db[tableName].find(query);
  
          if (!didCancel) {
            dispatch({ type: dataFetch.FETCH_SUCCESS, payload: result });
          }
        } catch (error) {
          if (!didCancel) {
            dispatch({ type: dataFetch.FETCH_FAILURE, payload: error });
          }
        }
      };
  
      fetchData().then(); // .then() gets rid of eslint warning
  
      return () =&gt; {
        didCancel = true;
      };
    }, [query, tableName, dbChangeFlag]);
  
    return [state, setQuery, setTableName];
  };
  
  </code></pre>