0

I have multiple tables, of small size, and I want to be able to write / read / update my components when the corresponding table has been updated by the app (we can consider it's a single user app for the moment).

I've been inspired by this question to write a custom Provider and associated hook for data fetching (and eventually posting) in my app: React useReducer async data fetch

I came up with this:

import React from "react";
import { useContext, useState, useEffect } from "react";
import axios from "axios";

const MetadataContext = React.createContext();

function MetadataContextProvider(props) {
  let [metadata, setMetadata] = useState({});

  async function loadMetadata(url) {
    let response = await axios.get(url);
    // here when I console.log the value of metadata I get {} all the time
    setMetadata({ ...metadata, [url]: response.data });
  }

  async function postNewItem(url, payload) {
    await axios.post(url, payload);
    let response = await axios.get(url);
    setMetadata({ ...metadata, [url]: response.data });
  }

  return (
    <MetadataContext.Provider value={{ metadata, loadMetadata, postNewItem }}>
      {props.children}
    </MetadataContext.Provider>
  );
}

function useMetadataTable(url) {
  // this hook's goal is to allow loading data in the context provider
  // when required by some component
  const context = useContext(MetadataContext);

  useEffect(() => {
    context.loadMetadata(url);
  }, []);

  return [
    context.metadata[url],
    () => context.loadMetadata(url),
    (payload) => context.postNewItem(url, payload),
  ];
}

function TestComponent({ url }) {
  const [metadata, loadMetadata, postNewItem] = useMetadataTable(url);
  // not using loadMetadata and postNewItem here

  return (
    <>
      <p> {JSON.stringify(metadata)} </p>
    </>
  );
}

function App() {
  return (
    <MetadataContextProvider>
      <TestComponent url="/api/capteur" />
      <br />
      <TestComponent url="/api/observation" />
    </MetadataContextProvider>
  );
}

export default App;

(the code should run in CRA context, both apis can be replaced with almost any API)

When I run it, a request is fired on both endpoints (/api/capteur and /api/observation), but where I'm expecting the metadata object in the MetadataContextProvider to have 2 keys: "/api/capteur" and "/api/observation", only the content of the last request made appears.

When I console.log metadata in the loadMetadata function, metadata always has the initial state hook value, that is {}.

I'm fairly new to React, I tried hard and I'm really not figuring out what's going on here. Can anyone help?

Bennett Dams
  • 6,463
  • 5
  • 25
  • 45
Hugo Merzisen
  • 303
  • 2
  • 8

1 Answers1

1

Your problem is how you update the metadata object with setMetadata. The operation of updating the metadata object via loadMetadata in your context is done by two "instances" respectively: TestComponent #1 and TestComponent #2. They both have access to the metadata object in your context, but they're not instantly synchronized, as useState's setter function works asynchronously.

The easy solution for your problem is called functional updates. useState's setter does also provide a callback function, which will then use (I'm oversimplifying here) the "latest" state.

In your context provider:

async function loadMetadata(url) {
  let response = await axios.get(url);
  setMetadata((existingData) => ({ ...existingData, [url]: response.data }));
  // instead of
  // setMetadata({ ...metadata, [url]: response.data });
}

Here is a working CodeSandbox: https://codesandbox.io/s/elegant-mclean-syiol?file=/src/App.js

Look at the console to see the order of execution.


I highly recommend to fully read React hooks documentation, especially the "Hooks API Reference". There are also other problems with your code (for example missing dependencies in the useEffect hook, do you have ESLint enabled?).

If you want to have a better overview on how to use React's context I can recommend Kent C. Dodds' blog:

https://kentcdodds.com/blog/application-state-management-with-react

https://kentcdodds.com/blog/how-to-use-react-context-effectively

Bennett Dams
  • 6,463
  • 5
  • 25
  • 45
  • 1
    It works like a charm thank you. I already went through hooks documentation, but it looks like I skipped some useful info I'm digging again. I do have ESLint enabled, it points missing dependencies, and is happy when the second arguments to useEffect is [context, url]. But if I do so, I end up in an infinite loop of updates. How am I supposed to do? – Hugo Merzisen Nov 20 '20 at 16:05
  • @HugoMerzisen The infinite loop is caused by the context's `loadMetadata` function. Every rerender will recreate the function, which will cause another rerender and so on... This is a topic for itself. You can fix this by using `useMemo` for your context value (this one: ``), or, in your case, by using `useCallback` for your `loadMetadata` function, which will result in not creating the function every render (Google "useCallback referential equality"): https://codesandbox.io/s/cold-lake-z1wgk?file=/src/App.js – Bennett Dams Nov 25 '20 at 10:10