javascriptreact-reduxredux-thunk

What is best practice to remove duplicate/redundant API requests?


So I am working in a React platform that has data that updates every second(I would like to move to web-sockets but its currently only supports gets).Currently, each component makes a fetch request for itself to get the data for the widget. Because the fetch requests are built into the widgets there are redundant api requests for the same data. I am looking for a possible better solution to remove these redundant api requests.

The solution I came up with uses what I call a data service that checks for any subscription to data sources then makes those api calls and places the data in a redux state for the components to then be used. I am unsure if this is the best way to go about handling the issue I am trying to avoid. I don't like how I need an interval to be run every second the app is running to check if there are "subscriptions". I am unsure if thats the correct way to go about it. With this solution I don't duplicate any requests and can add or remove a subscription without affecting other components.

One more thing, the id can change and will change what data I recieve

Here is a simplified version of how I am handling the service.

const reduxState = {
 id: "specific-id",  
 subscriptions: {
    sourceOne: ["source-1-id-1", "source-1-id-2", "source-1-id-3"],
    sourceTwo: ["source-2-id-1", "source-one-id-2"],
  },
  data: {
    sourceOne: { id: "specific-id", time: "milliseconds", data: "apidata" },
    sourceTwo: { id: "specific-id", time: "milliseconds", data: "apidata" },
  },
};

const getState = () => reduxState; //Would be a dispatch to always get current redux state

const dataService = () => {
  const interval = setInterval(() => {
    const state = getState();
    if (state.subscriptions.sourceOne.length > 0)
      fetchSourcOneAndStoreInRedux();
    if (state.subscriptions.sourceTwo.length > 0)
      fetchSourceTwoAndStoreInRedux();
  }, 1000);
};

const fetchSourcOneAndStoreInRedux = (id) =>{
    return async dispatch => {
        try {
            const res = await axios.get(`/data/one/${id}`) 
            dispatch(setSourceOneDataRedux(res.data))
        } catch (err) {
            console.error(err)
        }
    }
}

I am building my components to only show data from the correct id.


Solution

  • Here is a simple working example of a simple "DataManager" that would achieve what you are looking for.

    class DataManager {
      constructor(config = {}) {
        this.config = config;
        console.log(`DataManager: Endpoint "${this.config.endpoint}" initialized.`);
        if (this.config.autostart) { // Autostart the manager if autostart property is true
          this.start();
        }
      }
    
      config; // The config object passed to the constructor when initialized
      fetchInterval; // The reference to the interval function that fetches the data
      data; // Make sure you make this state object observable via MOBX, Redux etc so your component will re-render when data changes.
      fetching = false; // Boolean indicating if the APIManager is in the process of fetching data (prevent overlapping requests if response is slow from server)
    
      // Can be used to update the frequency the data is being fetched after the class has been instantiated
      // If interval already has been started, stop it and update it with the new interval frequency and start the interval again
      updateInterval = (ms) => {
        if (this.fetchInterval) {
          this.stop();
          console.log(`DataManager: Updating interval to ${ms} for endpoint ${this.config.endpoint}.`);
          this.config.interval = ms;
          this.start();
        } else {
          this.config.interval = ms;
        }
        return this;
      }
    
      // Start the interval function that polls the endpoint
      start = () => {
        if (this.fetchInterval) {
          clearInterval(this.fetchInterval);
          console.log(`DataManager: Already running! Clearing interval so it can be restarted.`);
        }
    
        this.fetchInterval = setInterval(async () => {
          if (!this.fetching) {
            console.log(`DataManager: Fetching data for endpoint "${this.config.endpoint}".`);
            this.fetching = true;
            // const res = await axios.get(this.config.endpoint); 
            // Commented out for demo purposes but you would uncomment this and clear the anonymous function below
            const res = {};
            (() => {
              res.data = {
                dataProp1: 1234,
                dataProp2: 4567
              }
            })();
            this.fetching = false;
            this.data = res.data;
          } else {
            console.log(`DataManager: Waiting for pending response for endpoint "${this.config.endpoint}".`);
          }
        }, this.config.interval);
    
        return this;
      }
    
      // Stop the interval function that polls the endpoint
      stop = () => {
        if (this.fetchInterval) {
          clearInterval(this.fetchInterval);
          console.log(`DataManager: Endpoint "${this.config.endpoint}" stopped.`);
        } else {
          console.log(`DataManager: Nothing to stop for endpoint "${this.config.endpoint}".`);
        }
        return this;
      }
    
    }
    
    const SharedComponentState = {
      source1: new DataManager({
        interval: 1000,
        endpoint: `/data/one/someId`,
        autostart: true
      }),
      source2: new DataManager({
        interval: 5000,
        endpoint: `/data/two/someId`,
        autostart: true
      }),
      source3: new DataManager({
        interval: 10000,
        endpoint: `/data/three/someId`,
        autostart: true
      })
    };
    
    setTimeout(() => { // For Demo Purposes, Stopping and starting DataManager.
      SharedComponentState.source1.stop();
      SharedComponentState.source1.updateInterval(2000);
      SharedComponentState.source1.start();
    }, 10000);
    
    // Heres what it would look like to access the DataManager data (fetched from the api)
    // You will need to make sure you pass the SharedComponentState object as a prop to the components or use another React mechanism for making that SharedComponentState accessible to the components in your app
    // Accessing state for source 1: SharedComponentState.source1.data
    // Accessing state for source 2: SharedComponentState.source2.data
    // Accessing state for source 3: SharedComponentState.source3.data

    Basically, each instance of the DataManager class is responsible for fetching a different api endpoint. I included a few other class methods that allow you to start, stop and update the polling frequency of the DataManager instance.