Reactjs Screen not refreshing, always one loop behind when updating - reactjs

I am updating a record that is being displayed on the screen. Yet once the record is updated in the MySql database, the screen does not reflect the update. I am seeking a way to force the refresh. In the code below, I update the record and reload the array and filter the results. Yet the screen does not get updated. If I click the browsers refresh, the screen displays with the updated data. If I do a second or third update, it always shows the results from the prior update, always one update behind.
I am new to Reactjs so I am hoping this is obvious to someone.
...
seqUp(contacttoassetrelationshipid){
ContactAssetMasterService.getContactToAssetRelationshipById(contacttoassetrelationshipid).then( (res) =>{
let contacttoassetrelationship = res.data;
contacttoassetrelationship.contacttoassetRelationshipSequence = contacttoassetrelationship.contacttoassetRelationshipSequence + 1;
ContactAssetMasterService.updateContactToAssetRelationship(contacttoassetrelationship, contacttoassetrelationshipid);
});
ContactAssetMasterService.getContactToAssetRelationship().then((res)
=> {
this.setState({ contacttoassetrelationship: res.data});
this.setState({contacttoassetrelationship: this.state.contacttoassetrelationship.filter(contacttoassetrelationship
=> contacttoassetrelationship.assets_assetsid === this.props.match.params.assetsid)});
}); };
...

There are 2 problems with your code.
updateContactToAssetRelationship returns a promise, which is an asynchronous operation. Just like
you did by calling then for getContactToAssetRelationshipById
and getContactToAssetRelationship, you need to wait until it
finishes before fetching the updated data. Otherwise, execution
continues right away (without waiting update call to finish) and
getContactToAssetRelationship fetches out-dated data.
setState is an asynchronous operation too. Plus, multiple calls of setState merged into one batch call. In the following case, for example, count will be updated only once:
// assume this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// count will be 1
Final code (removed/renamed variables just for brievity):
seqUp(contacttoassetrelationshipid){
ContactAssetMasterService.getContactToAssetRelationshipById(contacttoassetrelationshipid).then((res) =>{
res.data.contacttoassetRelationshipSequence++;
ContactAssetMasterService.updateContactToAssetRelationship(res.data, contacttoassetrelationshipid).then(() => {
ContactAssetMasterService.getContactToAssetRelationship().then((res1) => {
this.setState({contacttoassetrelationship: res1.data.filter(c => c.assets_assetsid === this.props.match.params.assetsid)});
});
});
});
};

Related

Save a function in state and update it periodically

The getDocsRealtime() adds a Firebase listener for a query and returns an unsubscribe function. In componentDidMount, this function loads top 5 documents initially. Every time the page is scrolled to bottom, this function is executed again to get the next 5 documents, and returns another unsubscribe function for the new documents. I want to make it so that:
unsubscribe function = old unsubscribe function + new unsubscribe function
Here is my code:
componentDidMount = async () => {
let unsubscribe = await getDocsRealtime()
this.setState({ unsubscribe })
// Add Listener when page is scrolled to bottom
window.addEventListener('scroll', this.onScrollToBottom)
}
onScrollToBottom = async () => {
if (window.scrollY > placeholderTop) {
let unsubscribe = await getDocsRealtime()
unsubscribe = () => {
this.state.unsubscribe()
unsubscribe()
}
this.setState({ unsubscribe })
}
}
componentWillUnmount = () => {
// Unsubscribe Listener
this.state.unsubscribe && this.state.unsubscribe
// Remove Listener for when page is scrolled to bottom
window.removeEventListener('scroll', this.onScrollToBottom)
}
The problem I am having here is that instead of calling the old unsubscribe function, the program is calling the unsubscribe function stored in the state and I get RangeError: Maximum call stack size exceeded error.
Every time the page is scrolled to bottom, this function is executed
again to get the next 5 documents
Since you want to query the database each time a specific event occurs in your front-end, i.e. the scrolling to the bottom, you should not, IMO, use a listener to query the database.
I would suggest you paginate the queries by "combining query cursors with the limit() method" as explained in the doc. With this technique, each time the user scrolls down, you construct a new query starting at the last previously visible document and you execute it once (instead of continually "listening to it").
I understand that it seems interesting to combine realtime listening to the DB with pagination but in reality it can create weird situations when a document is shown twice in the front end because a new document has been added in the middle of a set of previously displayed docs.

Isn't this validation synchronous?

Taking a look at this commit in the Formik.tsx file, the validation was moved from (in the case of touched as an example):
React.useEffect(() => {
if (
prevState.touched !== state.touched &&
!!validateOnBlur &&
!state.isSubmitting &&
isMounted.current != null
) {
const [validate, cancel] = makeCancelable(validateForm());
validate.then(x => x).catch(x => x); // catch the rejection silently
return cancel;
}
return;
}, [
prevState.touched,
state.isSubmitting,
state.touched,
validateForm,
validateOnBlur,
isMounted,
]);
to here:
const setFieldTouched = React.useCallback(
(
field: string,
touched: boolean = true,
shouldValidate: boolean = true
) => {
dispatch({
type: 'SET_FIELD_TOUCHED',
payload: {
field,
value: touched,
},
});
return validateOnBlur && shouldValidate
? validateFormWithLowPriority(state.values)
: Promise.resolve();
},
[validateFormWithLowPriority, state.values, validateOnBlur]
);
Jumping to the current implementation, it looks like this:
const setFieldTouched = useEventCallback(
(field: string, touched: boolean = true, shouldValidate?: boolean) => {
dispatch({
type: 'SET_FIELD_TOUCHED',
payload: {
field,
value: touched,
},
});
const willValidate =
shouldValidate === undefined ? validateOnBlur : shouldValidate;
return willValidate
? validateFormWithHighPriority(state.values)
: Promise.resolve();
}
);
But why and how do the latter two ensure that the validation will be run with the "freshest" values? With the effect you might dispatch a change, then a blur (both will change the state). The state will then change at some point causing the effect to then run with the current values.
In the latter, the validation - when following the trail of calls - should still run synchronously (executor function of a promise is executed synchronously) - only the setting of the errors will wait until the promise is done (has explicit then). This should mean that when i call setFieldValue and then setFieldTouched immediately after, that the state will still be the old one (since state change is asynchronous). An example would be that i could dispatch a change to a state, and try to access the same state immediately after, which will still be the old state (code sandbox).
However when looking at this code sandbox, the values passed to validate (from the validation flow in setFieldTouched are the actual fresh values. I don't see why this is the case...this might very well be a mistaken interpretation of the flow on my side, but that is why i pose this question.
Edit
Turns out, the whole question stemmed from codesandbox using an old version of formik#1.3.2, which has different behavior from the current version of Formik. This led to confusion about the expected behavior from reading the source of formik#2.1.6 but seeing behavior that did not match the source.
It's a classic pitfall, reading or editing one source file, but executing another.
Original answer below:
You are asking a bunch of very interesting questions. I'll try to answer all of them, but first I want to talk a bit about how React rendering works in general, as that will inform the answers below.
First, let's look at this code example:
export default function App() {
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => count * 2, [count]);
const buttonRef = useRef();
return (
<div className="App">
<button
ref={buttonRef}
onClick={() => {
setCount(count + 1);
console.log(count, doubleCount, buttonRef.current.textContent);
}}
>
{doubleCount}
</button>
</div>
);
}
(based on: https://twitter.com/RyanCarniato/status/1353801009844240389/photo/1)
Code Sandbox
Clicking the button 3 times prints:
0 0 "0"
1 2 "2"
2 4 "4"
This illustrates an important point - at the point where the click event is executed and you call setState(count + 1), the setState() operation itself has not been executed - it is instead scheduled as a change that will be resolved by the next React render. The value of count variable has not immediately increased. This is because the onClick() function was created when the last render happened, and it has captured whatever value count had at that time.
You must treat variables that come from React hooks like useState(), useReducer() and useMemo() as immutable - they do not change inline, they will only be updated with the next render.
This means that calling setState() itself a few times might also not do exactly what you expect. Let's say we start with count = 0.
setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
setCount(count + 1); // same as setCount(0 + 1)
console.log(count) // 0, it will be 1 in the next render
If you wanted a value to depend on a previously set value, you should do:
setCount(count => count + 1); // same as setCount(0 => 0 + 1)
setCount(count => count + 1); // same as setCount(1 => 1 + 1)
setCount(count => count + 1); // same as setCount(2 => 2 + 1)
console.log(count) // still 0 here, but it will be 3 in the next render
With the above in mind, we can answer the following question:
However when looking at this code sandbox, the values passed to validate (from the validation flow in setFieldTouched are the actual fresh values. I don't see why this is the case...this might very well be a mistaken interpretation of the flow on my side, but that is why I pose this question.
The reason you see different values is that you see the values variable is what it was at the time you clicked the button, and before setFieldValue() has executed, as it behaves like setCount() in the example above. Also, the code sandbox runs Formik 1.3.2, I've explained why that version is special below. You wouldn't see fresh values with the latest version.
How does validate() get the correct data if the values data in React hasn't updated yet?
The value passed to the validate callback of Formik is computed by Formik itself on the fly, as part of the event handler! This is done so that the validation errors can also be calculated without waiting on the rerender of the value. It's basically a performance optimization, and not strictly necessary. Still, let's dive in:
What is useEventCallback()?
You'll see this sprinkled throughout Formik code. All it is is a mild performance optimization - the variables inside that function will always be the variables from the current render, but the function will have a stable referential value. That simply means that setFieldValue from one render is strictly equal (===) to setFieldValue from another render. This is used to optimize some memoization techniques.
In practice, every time you see useEventCallback() you should mentally imagine it's just a normal function;
How does setFieldTouched() ensure that the validation will be run with the "freshest" values?
It doesn't in Formic 2.x! setFieldTouched() runs against whatever values were last rendered.
Warning: The next section explores the behavior of React Class Components. They are not recommended for new projects, React Hooks should be used instead
In Formik 1.x however, setFieldTouched() added validations to be ran as a callback with second parameter to React.Component.setState() - this callback executes AFTER the next render.
So what was happening there was, setFieldTouched() queued this callback, later setFieldValue() updated the value, and then after React rerendered it executed the callback queued by setFieldTocuhed().
How does setFieldValue() work and why does validate() have fresh values?
(This is based on Formik 2.1.6)
I will now inline and comment all of the relevant code that is run in setFieldValue():
const setFieldValue = (field: string, value: any, shouldValidate?: boolean) => {
// use dispatch() from React.useReducer() to update the value of the field and trigger a next render
dispatch({
type: 'SET_FIELD_VALUE',
payload: {
field,
value,
},
});
// from here on out I've inlined the code of validateFormWithHighPriority()
// set IS_VALIDATING state on the form.
// This change executes as part of the above render, alongside the value change!
dispatch({ type: 'SET_ISVALIDATING', payload: true });
// create a fresh copy of `values` with the updated field
const updatedValues = setIn(state.values, field, value);
Promise.all(
// validation functions go here; They will run with the `updatedValues`
// check out the functions `runAllValidations()` and ``
validate(updatedValues)
).then(combinedErrors => {
// this code is guaranteed to execute AFTER React has rendered the `SET_ISVALIDATING` state above,
// and will trigger a NEW React render
dispatch({ type: 'SET_ISVALIDATING', payload: false });
dispatch({ type: 'SET_ERRORS', payload: combinedErrors });
});
}
In effect, what is happening is that setFieldValue() both triggers a new React render, and also calculates what values would be in that new render with setIn(state.values, field, value). It then passes that to validate(), though it does it in a somewhat roundabout way.
I hope this was helpful and that I was able to answer your questions. If there is anything unclear ask away in the comments

Issues accessing react state in firestore onSnapshot listener

I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.

React Initial Render Success vs Second Render Failure?

I have an inventory component linked to an inventory Firebase. When my component renders for the first time it works perfectly. When I leave the inventory page and come back, however, my component only renders the last item in the inventory database. It is also weird because when I include the console.log('add fired ' + product.data.name); it recognizes every node in the database and logs the product name, but they are still not displayed on the screen. Any help would be greatly appreciated.
componentWillMount(){
//Create reference to inventory db
this.inventory = firebase.database().ref('inventory');
this.inventory.on('child_added', snapshot => {
let product = { data: snapshot.val(), id: snapshot.key };
console.log('add fired ' + product.data.name);
this.setState({ products: this.state.products.concat(product) });
});
}
https://stackoverflow.com/a/40199596/6554834.
componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.

Update value of variable from Firebase query?

I currently am trying to pull the value of an item from a firebase using once, and use this value to populate the var itemsList. However, although itemsList is correctly populated within the once call, it is logging as undefined outside the call. I think this may have something to do with the asynchronous nature of Firebase, but I'm not sure how to remedy this problem. Any suggestions?
submitAnswer: function() {
var userRef = usersRef.child(key);
//get items_list for user
var itemsList;
userRef.once('value', (snap) => {
itemsList = snap.val().items_list;
console.log('items list populated here' + itemsList);
});
console.log("items list not populated here" + itemsList);
},
You're correct about the Firebase call, it's asynchronous and the code isn't going to wait for it to complete before logging the unpopulated itemsList.
If you're only looking to do something simple with the data, just be sure to check that it exists before performing any action with it (and handle it like you would any async data).
if(itemsList){
console.log('My items have arrived! ' + itemsList);
}
If that data is going to be propagated further down your app it is usually suggested to make a call to setState() with your response data from Firebase to trigger a re-render of your components with the new data you just fetched.
So something along the lines of:
userRef.once("value", (snap) => {
itemsList = snap.val().items_list;
this.setState({
items: itemsList;
});
}.bind(this));

Resources