Problem with custom ContextProvider state when using custom hook for fetching data in multiple tables in React

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?

1 answer

  • answered 2020-11-20 14:34 Bennett Dams

    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