How to create a variable that sets data asynchronously and is shared across many instances of a hook without multiple asynchronous calls?

Viewed 81

I'm building a chart component that needs an array of ranges, something like

[{min: 0, max: 9}, {min: 10, max: 29}, {min: 30, max: 49}]

I'm getting this data from a service in the backend. I thought about creating a hook useRanges() that will make the request to the backend, store the result in its state and the return it.

The thing is that since this data won't change, like never, I can't be making more than one request to the backend. Otherwise that could lead to perfoemance issues.

Am I able to create some variable that could be shared among the instances of this hook?

3 Answers

Disclosure: I am the author of the suspense-service library used in this answer.

You can use a React Suspense integration like suspense-service to declaratively separate fetching data from consuming data like this:

const { createService, useService } = SuspenseService;

const fetchData = (value) => {
  console.log(`fetching ${value}`); // will be called only once

  return new Promise(
    (resolve) => { setTimeout(resolve, 1000); }
  ).then(
    () => [{min: 0, max: 9}, {min: 10, max: 29}, {min: 30, max: 49}]
  );
};

const Ranges = createService(fetchData);
const useRanges = () => useService(Ranges);

// Component that depends on range data
const ComponentHavingRangeData = () => {
  const range = useRanges();

  return (
    <pre>range: {JSON.stringify(range)}</pre>
  );
}

// Group of components that depend on range data
const App = () => (
  <React.StrictMode>
    <Ranges.Provider
      request="/api/chart/ranges"
      fallback="fetching data..."
    >
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
    </Ranges.Provider>
  </React.StrictMode>
);

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/suspense-service@0.2.8/dst/umd/suspense-service.js"></script>
<div id="root"></div>

Note that the useRanges() hook here is not initiating the request to the backend, it is consuming the response from the request initiated by the <Ranges.Provider> component.

This questions seems simple on the surface.

Note that we are waiting for the asynchronous call to complete before showing the child-components.

// useRanges Hook
const getData = () => new Promise((resolve) => {
  console.log('making asynchronous call for data'); //will be called only once
  setTimeout(() => {
    const data = [{min: 0, max: 9}, {min: 10, max: 29}, {min: 30, max: 49}]
    resolve({
      data,
      status: 200
    })
  }, 1000) // usually this call will take much less than 1000ms
})

let defaultData = null;

const useRanges = () => {
  const [range, setRange] = React.useState(defaultData);
  React.useEffect(() => {
    if(!range || !range.length) {
      getData()
        .then((res) => {
          defaultData = res.data;
          setRange(defaultData);
        })
    }
  }, [range])
  return range
}

// Component that depends on range data
const ComponentHavingRangeData = () => {
  const range = useRanges()
  return <pre>range: {JSON.stringify(range)}</pre>
}

// Group of components that depend on range-data
function App() {
  const range = useRanges()
  return (
    <div>
    {range && <div className="App">
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
      <ComponentHavingRangeData />
    </div>
  }
  {!range && 'fetching data :)'}
  </div>
  );
}

// Render App
const root = document.getElementById('root')
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Good Luck...

Related