I have been stumped on a work around for this problem for a while now and was hoping someone could help.
I am currently working on a React UI that sends info to the backend Firebase for a budgeting app.
When the page first loads, I pull in the data using this:
const [incomeSources, setIncomeSources] = React.useState([]);
/////////////////////////////////
// PULL IN DATA FROM FIREBASE //
///////////////////////////////
async function getData() {
const doc = await getDoc(userCollectionRef);
const incomesData = doc.data().incomeSources;
// const expensesData = doc.data().expenses;
// const savingsData = doc.data().savingsAllocation;
// SET STATES //
if (incomesData.length > 0) {
setIncomeSources(incomesData);
}
}
then when I want to add a new object to the state array I use a input and button. The issue I currently have is that I have it set up like this:
async function updateFirebaseDocs(userID, stateName, state) {
const userRef = doc(db, "users", userID);
try {
await setDoc(userRef, { [stateName]: state }, { merge: true });
} catch (err) {
console.error("error adding document", err);
}
}
React.useEffect(() => {
updateFirebaseDocs(userID, 'incomeSources', incomeSources)
},[incomeSources])
this works so long as I don't refresh the page, because upon page refresh, incomeSources defaults back to an empty array on render. Causing firebase docs to become an empty array again which deletes firestore data.
I can't for the life of me figure out the workaround even though I know its probably right in front of me. Can someone point me in the right direction please.
Brief summary: I am able to pull in data from backend and display it, but I need a way to keep the backend database up to date with changes made in the Frontend. And upon refreshing the page, I need the data to persist so that the backend doesn't get reset.
Please advise if more information is needed. First time posting.
I have tried using the above method using useEffects dependency, I have also tried using localstorage to work around this but also don't can't think of a way of implementing it. I feel I am tiptoeing around the solution.
I have a chrome extension that stores data in Firestore and populates that data to the frontend. I always have to refresh the page to see newly added data, which isn’t a user friendly experience. How can I update the UI to show the newly updated data without having to refresh the page?
So far, I've tried using useEffect to get the data. Inside of it, I'm using a function that gets data from Firestore cached inside of chrome local storage.
Here is my code
const getFolderData = () => {
getDataFromChrome("docId").then((res: any) => {
setDocId(res.docId);
});
getDataFromChrome("content").then((res: any) => {
//console.log("getting in mainfolder",res);
// for (const item of res.content) {
// if (item.type.toLowerCase() === "subfolder") {
// // console.log(item)
// getSubFolder(item.id);
// }
// }
for (const item of res.content) {
setTiersContent((pre: any) => [...pre, item]);
}
});
};
useEffect(() => {
getFolderData();
}, []);
I also get this error. I'm also using the chrome extension API to communicate with a background script. It could be related to the problem
Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received
I've never used firebase so I'm not sure what your functions do, I can only guess. A few things wrong from what I can see:
Your useEffect is set to only run on page load since the dep array is empty, I assume you want to refetch on some condition.
If any of the 2 functions is supposed to be a subscription, your useEffect needs to return a cancel function.
Refetch data when needed is not a new problem, packages like React Query has tools that optimize your requests and refetch when needed. I suggest you give it a shot if your app has more than 2-3 fetch requests.
I have implemented a table using ag-grid react. I fetch data from an api to fill in that table.
const getDataForTable = async () => {
try {
//apis to fetch the data
setGridData(apiData);
}
catch (err) {
console.error(err);
}
}
useEffect(() => {
getDataForTable();
}, []);
Now, I have also created an onClick method for deleting selected rows of the table. I am removing the rows from api as well. Once the rows are deleted, I just want to refresh the grid with updated data. Currently it only works if I explicitly reload the page.
const onClickRemoveRowsByIds = async () => {
selectedRows.forEach(d => {
listOfIds.push(d.Id);
});
if (window.confirm("Are you sure ?")) {
await listOfIds.map((ele) => removeActiveList(ele));
getDataForTable()
}
}
But when I make a call to getDataForTable function, I get bad request error for the apis. On looking at the reponse body of the api : I get Invalid character found in method name. HTTP method names must be tokens. The authToken and rest of the information remains same but still fetch is not working again. Am I missing some step, or doing it completely wrong? The delete works fine, just the refresh is not happening.
I have to pretty weird case to handle.
We have to few boxes, We can call some action on every box. When We click the button inside the box, we call some endpoint on the server (using axios). Response from the server return new updated information (about all boxes, not the only one on which we call the action).
Issue:
If user click submit button on many boxes really fast, the request call the endpoints one by one. It's sometimes causes errors, because it's calculated on the server in the wrong order (status of group of boxes depends of single box status). I know it's maybe more backend issue, but I have to try fix this on frontend.
Proposal fix:
In my opinion in this case the easiest fix is disable every submit button if any request in progress. This solution unfortunately is very slow, head of the project rejected this proposition.
What we want to goal:
In some way We want to queue the requests without disable every button. Perfect solution for me at this moment:
click first button - call endpoint, request pending on the server.
click second button - button show spinner/loading information without calling endpoint.
server get us response for the first click, only then we really call the second request.
I think something like this is huge antipattern, but I don't set the rules. ;)
I was reading about e.g. redux-observable, but if I don't have to I don't want to use other middleware for redux (now We use redux-thunk). Redux-saga it will be ok, but unfortunately I don't know this tool. I prepare simple codesandbox example (I added timeouts in redux actions for easier testing).
I have only one stupid proposal solution. Creating a array of data needs to send correct request, and inside useEffect checking if the array length is equal to 1. Something like this:
const App = ({ boxActions, inProgress, ended }) => {
const [queue, setQueue] = useState([]);
const handleSubmit = async () => { // this code do not work correctly, only show my what I was thinking about
if (queue.length === 1) {
const [data] = queue;
await boxActions.submit(data.id, data.timeout);
setQueue(queue.filter((item) => item.id !== data.id));
};
useEffect(() => {
handleSubmit();
}, [queue])
return (
<>
<div>
{config.map((item) => (
<Box
key={item.id}
id={item.id}
timeout={item.timeout}
handleSubmit={(id, timeout) => setQueue([...queue, {id, timeout}])}
inProgress={inProgress.includes(item.id)}
ended={ended.includes(item.id)}
/>
))}
</div>
</>
);
};
Any ideas?
I agree with your assessment that we ultimately need to make changes on the backend. Any user can mess with the frontend and submit requests in any order they want regardless how you organize it.
I get it though, you're looking to design the happy path on the frontend such that it works with the backend as it is currently.
It's hard to tell without knowing the use-case exactly, but there may generally be some improvements we can make from a UX perspective that will apply whether we make fixes on the backend or not.
Is there an endpoint to send multiple updates to? If so, we could debounce our network call to submit only when there is a delay in user activity.
Does the user need to be aware of order of selection and the impacts thereof? If so, it sounds like we'll need to update frontend to convey this information, which may then expose a natural solution to the situation.
It's fairly simple to create a request queue and execute them serially, but it seems potentially fraught with new challenges.
E.g. If a user clicks 5 checkboxes, and order matters, a failed execution of the second update would mean we would need to stop any further execution of boxes 3 through 5 until update 2 could be completed. We'll also need to figure out how we'll handle timeouts, retries, and backoff. There is some complexity as to how we want to convey all this to the end user.
Let's say we're completely set on going that route, however. In that case, your use of Redux for state management isn't terribly important, nor is the library you use for sending your requests.
As you suggested, we'll just create an in-memory queue of updates to be made and dequeue serially. Each time a user makes an update to a box, we'll push to that queue and attempt to send updates. Our processEvents function will retain state as to whether a request is in motion or not, which it will use to decide whether to take action or not.
Each time a user clicks a box, the event is added to the queue, and we attempt processing. If processing is already ongoing or we have no events to process, we don't take any action. Each time a processing round finishes, we check for further events to process. You'll likely want to hook into this cycle with Redux and fire new actions to indicate event success and update the state and UI for each event processed and so on. It's possible one of the libraries you use offer some feature like this as well.
// Get a better Queue implementation if queue size may get high.
class Queue {
_store = [];
enqueue = (task) => this._store.push(task);
dequeue = () => this._store.shift();
length = () => this._store.length;
}
export const createSerialProcessor = (asyncProcessingCallback) => {
const updateQueue = new Queue();
const addEvent = (params, callback) => {
updateQueue.enqueue([params, callback]);
};
const processEvents = (() => {
let isReady = true;
return async () => {
if (isReady && updateQueue.length() > 0) {
const [params, callback] = updateQueue.dequeue();
isReady = false;
await asyncProcessingCallback(params, callback); // retries and all that include
isReady = true;
processEvents();
}
};
})();
return {
process: (params, callback) => {
addEvent(params, callback);
processEvents();
}
};
};
Hope this helps.
Edit: I just noticed you included a codesandbox, which is very helpful. I've created a copy of your sandbox with updates made to achieve your end and integrate it with your Redux setup. There are some obvious shortcuts still being taken, like the Queue class, but it should be about what you're looking for: https://codesandbox.io/s/dank-feather-hqtf7?file=/src/lib/createSerialProcessor.js
In case you would like to use redux-saga, you can use the actionChannel effect in combination with the blocking call effect to achieve your goal:
Working fork:
https://codesandbox.io/s/hoh8n
Here is the code for boxSagas.js:
import {actionChannel, call, delay, put, take} from 'redux-saga/effects';
// import axios from 'axios';
import {submitSuccess, submitFailure} from '../actions/boxActions';
import {SUBMIT_REQUEST} from '../types/boxTypes';
function* requestSaga(action) {
try {
// const result = yield axios.get(`https://jsonplaceholder.typicode.com/todos`);
yield delay(action.payload.timeout);
yield put(submitSuccess(action.payload.id));
} catch (error) {
yield put(submitFailure());
}
}
export default function* boxSaga() {
const requestChannel = yield actionChannel(SUBMIT_REQUEST); // buffers incoming requests
while (true) {
const action = yield take(requestChannel); // takes a request from queue or waits for one to be added
yield call(requestSaga, action); // starts request saga and _waits_ until it is done
}
}
I am using the fact that the box reducer handles the SUBMIT_REQUEST actions immediately (and sets given id as pending), while the actionChannel+call handle them sequentially and so the actions trigger only one http request at a time.
More on action channels here:
https://redux-saga.js.org/docs/advanced/Channels/#using-the-actionchannel-effect
Just store the promise from a previous request and wait for it to resolve before initiating the next request. The example below uses a global variable for simplicity - but you can use smth else to preserve state across requests (e.g. extraArgument from thunk middleware).
// boxActions.ts
let submitCall = Promise.resolve();
export const submit = (id, timeout) => async (dispatch) => {
dispatch(submitRequest(id));
submitCall = submitCall.then(() => axios.get(`https://jsonplaceholder.typicode.com/todos`))
try {
await submitCall;
setTimeout(() => {
return dispatch(submitSuccess(id));
}, timeout);
} catch (error) {
return dispatch(submitFailure());
}
};
I have a redux action set up that posts to an external API, this updates a database, and returns the updated results. I then run another function inside to check a database table for new results:
this.props.updateAddTest(payload)
.then((response) => {
if (response.error) {
} else {
let payloadTwo = {
parentTestId: this.state.parentTestId,
bespokeTestId: response.response.testId,
selectedTests: selectedTests,
}
page.props.loadAvailableTests(payloadTwo)
.then((response) => {
page.setState({checkInvalidTests: response.response})
})
}
})
Running this code makes the network response time around 10 seconds - why does it take so long? Running the functions separately, it takes around 200ms. e.g just running:
this.props.updateAddTest(payload);
Why does nesting one redux action inside another slow it down so much?