Reputation: 870
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.
Upvotes: 2
Views: 7196
Reputation: 89
Try this lightweight fetch wrapper lib; the implementation is pretty neat:
import xior from 'xior';
import dedupePlugin from 'xior/plugins/dedupe';
import throttlePlugin from 'xior/plugins/throttle';
// Setup
const http = xior.create({
baseURL: 'http://example.com',
});
http.plugins.use(dedupePlugin()); // Prevent same GET requests from occurring simultaneously.
http.plugins.use(throttlePlugin()); // Throttle same `GET` request in 1000ms
//
// Dedupe the same `GET` requests, this will only sent 1 real request
await Promise.all([
http.get('/api/get-data-2'),
http.get('/api/get-data-2'),
http.get('/api/get-data-2'),
]);
More info from xior docs: https://github.com/suhaotian/xior
Upvotes: 0
Reputation: 498
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.
Upvotes: 2