Array of hooks in React
Asked Answered
G

2

6

I'm starting to use hooks in React and I got stuck, when I realized I would need an array of hooks to solve my problem. But according to the Rules of Hooks

Only Call Hooks at the Top Level

I'm not allow to call hooks inside a loop (and I guess also not in map).

My custom hook subscribes to an API and adds data to the state when there is an update:

export const useTrace = (id) => {
  [trace, setTrace] = useState([])

  useEffect(() => {
    Api.getCurrentTrace(id)
      .then(currentTrace => {
        setTrace(currentTrace)
      })
  }, [id])
  
  useEffect(() => {
    Api.subscribeTraceUpdate(onUpdateTrip)
    
    return () => {
      Api.unsubscribeTraceUpdate(onUpdateTrip)
    }
  }, [])

  const onUpdateTrip = msg => {
    if (msg.id === id) {
      setTrace([msg.data].concat(trace))
    }
  }
}

In my component I have a state with an array of IDs. For each ID I would like to use the useTrace(id) hook somehow like this:

import DeckGL from '@deck.gl/react'

function TraceMap({ ids }) {
  const data = ids.map((id) => ({
    id,
    path: useTrace(id)
  }))

  const pathLayer = new PathLayer({
    id: 'path-layer',
    data,
    getPath: d => d.path
  })

  return <DeckGL
    layers={[ pathLayer ]}
  />
}

For the sake of simplicity I got ids as a property instead of having a state.

Glosseme answered 8/1, 2021 at 13:58 Comment(1)
Maybe think about doing this in a more performant process. If you are using arrays to update state on any change instead do a test and compare the array data rather than your current intention.Uncourtly
B
1

Why not have a useTraces custom hook rather than useTrace. This new hook can take an array of ids instead of a single id.

export const useTraces = (ids) => {
  [traces, setTraces] = useState([]);

  useEffect(() => {
    (async () => {
      const traces = await Promise.all(
        ids.map((id) => Api.getCurrentTrace(id))
      );
      setTraces(traces);
    })();
  }, [ids]);

  // ... 
};
Bibber answered 8/1, 2021 at 14:7 Comment(4)
I'm using that hook also in an other component, where I only need the trace of an single ID. I agree, having a useTraces hook would solve that problem. The way you expressed it, would create an Api.getCurrentTraces call for every ID, when only one ID is added or removed.Glosseme
You can further optimise this hook to only send requests for changed ids. Regarding the other component, you can just send an array of single id.Bibber
I hoped that there is a way to use reacts mechanism somehow, where it compares the old IDs (keys) with the new ones and only update objects where the ID (key) changed.Glosseme
The problem here is what if you need to request a trace for a single ID? The public API for the hook becomes rather awkward to use (it's a good idea to show it, because this looks reasonable without the ugly calls to setTraces).Mesial
B
0

Another idea might be to create a sub component and use your hook in each of them.

Bentonbentonite answered 28/6 at 13:31 Comment(2)
What if the parent needs the state in aggregate, though? Then the children would need to pass their data up via a callback. Not terrible, but also breaches the "when state is shared, hoist it to the parent" rule of React.Mesial
If, yes, of course. But, I could not see any use by the parent in the given use case. But I totally agree, if the parent needs the state, the parent manages it. That's a basic principle in React.Bentonbentonite

© 2022 - 2024 — McMap. All rights reserved.