setState synchronously with reactn? - javascript

I'm having trouble with setState when using the package: reactn
When I replace these lines (1) with those lines (2), the code works. The (2) is a workaround, deals with asynchronous setState, but I want to understand why (1) doesn't work.
As I know, I can pass a callback function to setSomeState in React Hooks:
If the new state is computed using the previous state, you can pass a function to setState
This is also another usage of useGlobal from the document of reactn, which also uses a callback function as an argument for setGlobal. Why their example works but mine doesn't?
Full code: https://snack.expo.io/#loia5tqd001/d26e8f
Snippets:
listSymbols = [ "USD", "EUR", ... ]
usdRates = {} // expect to be usdRates = { USD: 1, EUR: 0.9 ... }
// getExchangeRate is in utils/utils.js
// => The code doesn't work
for (const symbol of listSymbols) {
getExchangeRate("USD", symbol).then(exchangeRate => {
setUsdRates(oldUsdRates => ({
...oldUsdRates,
[symbol]: exchangeRate
}))
.then(() => console.log("Call api getting exchange rate for " + symbol, usdRates) )
})
}
// => The code works as expected
for (const symbol of listSymbols) {
getExchangeRate("USD", symbol).then(exchangeRate => {
usdRates[symbol] = exchangeRate
console.log("Call api got exchange rate for " + symbol, usdRates)
})
}
setUsdRates(usdRates)

Based on the source of reactn it doesn't appear that the updater function style is supported when using the useGlobal('propertyName') version of the hook.
Here is the definition of the property setter: https://github.com/CharlesStover/reactn/blob/master/src/use-global.ts#L95
You can see it creates a newGlobalState object and passes it to setGlobal.
Then setGlobal calls the set on the global state manager here: https://github.com/CharlesStover/reactn/blob/master/src/global-state-manager.ts#L302
Since newGlobalState from the property setter is always an object, the updater version is never used.
You could achieve what you want by passing nothing to useGlobal and dealing with the entire state object instead, as per the example in the docs that you linked:
const [global, setGlobal] = useGlobal();
...
getExchangeRate("USD", symbol).then(exchangeRate => {
setGlobal(oldGlobal => ({
...oldGlobal,
usdRates: {
...oldGlobal.usdRates,
[symbol]: exchangeRate,
},
}))
.then(() => console.log("Call api getting exchange rate for " + symbol, usdRates) )
})
}
Also I'm not sure your other example is 100% correct - you're not waiting to call setUsdRates until all of the async getExchangeRate calls are complete.

Okay I will try to clear some things here
as you said
usdRates = {} // expect to be usdRates = { USD: 1, EUR: 0.9 ... }
So it's supposed to be an object
const [usdRates, setUsdRates] = useGlobal({})
then do
useEffect(() => {
for (const symbol of listSymbols) {
getExchangeRate("USD", symbol).then(exchangeRate => {
setUsdRates(oldUsdRates => ({
...oldUsdRates,
[symbol]: exchangeRate
}))
.then(() => console.log("Call api getting exchange rate for " + symbol, usdRates)
)
})
}
}, [])
Hope it helps

Related

Why do I see stale data even after invalidating my queries?

I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})

How to get the value of useRef current in React Native

I am developing a React Native application and am facing the following error:
I have defined a useRef which stores the doc ID from a firebase collection. But when I call that variable after it has been defined, the .current value returns a blank string.
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
console.log(bidId.current)
}
})
})
The above code returns the expected value. However, when I call the variable outside this db.collection loop, I get the following value:
But calling the bidId.current returns a blank string.
Please can someone help me with this. Thanks!
Actually this is what happens:
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
// This line gets executed after some time!
console.log(bidId.current)
}
})
})
// This gets executed first! (The value has not been stored yet!)
console.log(bidId.current);
Using the "useState" hook instead of "useRef" will solve the issue. Consider the following code:
const [BidId, setBidId] = useState<string | null>(null);
// This will store the value...
useEffect(() => {
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
setBidId(doc.id);
}
})
})
}, []);
// Here you can access the value
useEffect(() => {
if(BidId !== null)
console.log(BidId);
}, [BidId]);
// You can also return the component like the following:
return (<View>The Bid ID is: {BidId !== null ? BidId : "Loading..."}</View>);
Your useEffect basically says that whenever pageRef changes, call this function. If done outside, it will call do your tasks on every render instead of doing the whenever pageRef values is changed. Also, in initial renders, it may give undefined values.
You can only return a function in useEffect which basically says that before running the same next time, run this function before.
Try (currentUser without the '?' query character):
if (doc.data().email === auth.currentUser.email) {
bidId.current = doc.id
console.log(bidId.current)
}

Need to execute async function for every element of the array but dispatch action with progress only

I am quite new in RxJS, still trying to figure out how to implement different features using it.
I need help regarding the implementation of an observable, tried so many ways but none seems to work.
I have this function:
export function automateParameterEdit(tunId) {
const progress$ = new Subject();
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
progress$.next({ ...tun , progress: '0' });
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });
}),
catchError(e => {
// whatever
}),
finalize(async () => {
// whatever
})
)
)
);
return merge(progress$, process$);
}
So, right now, the action is being dispatched twice, once because of the progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 }); that emits the new tun progress, and the second time I believe it's because of the execution of: concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId))
Let's say there are 4 rules (Object.values(rules).length === 4). In the console I see 4 x 2 = 8 actions dispatched, half of them have invalid payload.
What I want to do is execute the autoEditParameters(tunId, rule.ruleId, tun.rulesetId) which btw is async, and after each execution I want to emit the progress (progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });).
How do I stop the invalid actions from being dispatched and only execute the async autoEditParameters and dispatch progress?
You don't need a Subject!
You only need a subject when you need to "manually" push a value through a stream. But, in your case, you just want to modify (map) emissions into a different shape.
So, you can just get rid of the subject. There's no need to merge process$ with progress$; you can simply return progress$;
function automateParameterEdit(tunId) {
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
return { ...tun, progress: progress / Object.values(rules).length * 100 };
})
)
)
);
return process$;
}
Here are a couple StackBlitz samples:
Original
Possible Solution
and after each execution I want to emit the progress
Not sure if you meant you just wanted emit the numeric percent (not an object), but that could easily be done. Sometimes breaking it down into smaller functions can make it easier to follow:
function automateParameterEdit(tunId): Observable<number> {
return updateTun(tunId).pipe(
flatMap(processRules)
);
}
function updateTun(tunId): Observable<Tun> {
return defer(async () => updateStatus(tunId, 'autoTun'))
}
function processRules(tun: Tun): Observable<number> {
return from(tun.rules).pipe(
concatMap(rule => autoEditParameters(tun.id, rule.ruleId, tun.rulesetId)),
scan(acc => acc + 1, 0),
map(doneCount => doneCount / tun.rules.length * 100),
startWith(0),
)
}
Here, updateTun() just wraps the async function and returns an observable, so it will be executed whenever it is subscribed to.
processRules() takes a Tun and returns an Observable<number> that is the progress percent. startWith just emits an intial value of 0.
StackBlitz

Testing React Components setState overload which takes a function

I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});

ReactJS - how do I update nested and "normal" state properties?

This is how my state looks like:
constructor(props, context) {
super(props, context);
this.state = {
show: false,
btnLabel: 'GO!',
car: {
owner: false,
manufacturer: false,
color: false
}
};
}
and this is how I modify state:
handleClickFetchPrice() {
this.setState({btnLabel: 'Fetching data...' });
console.log(this.state.fetchPriceBtn);
const url = 'some url';
axios.get(url)
.then(res => {
let car = [...this.state.car];
car.owner = res.data.owner;
car.manufacturer = res.data.manufacturer;
car.color = res.data.color;
this.setState({car});
})
}
The attribute car is updated, but fetchPriceBtn is not - the output of console.log(this.state.fetchPriceBtn); is still GO!.
What am I overlooking? Why the fetchPriceBtn is not updated?
React setState is an asynchronous process - you don't know exactly when it will be updated, you can only schedule the update.
To achieve your desired functionality, you can provide a callback into the setState method.
this.setState({ btnLabel: 'Fetching data...' }, () => console.log(this.state.fetchPriceBtn))
You can learn more following the documentation on the method.
#christopher is right, setState is an asynchronous process. But when second time call handleClickFetchPrice() function your btnLabel is value will be equal to Fetching data...
As answered in previous answers setState is asynchronous, so your console.log can't catch up the state change immediately. Again as suggested you can use callback function to track this change but if you use console.log just for debugging or want to see what changes in your state you can do this in your render function. And using a callback just for debug is not a nice way. Its purpose somehow different and if you check the official documentation, componentDidMount method is being suggested for such logic.
render() {
console.log( this.state.foo );
return (...)
}
If you do that you see two console.log output, one before state change and one after.
Also, your state operations might be enhanced. You car property is not an array, but you are converting it to an array and setting it? Is this what you intend:
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color } } ) );
})
Here we are not mutating our state directly, instead we are using spread operator and setting the desired properties. For your example we are setting the whole property actually.
One last note, I think you want to do that something like that:
this.setState( { btnLabel: "fetching } );
axios.get(url)
.then(res => {
const { owner, manufacturer, color } = res.data;
this.setState( prevState => ( { car: { ...prevState.car, owner, manufacturer, color }, btnLabel: "go" } ) );
})
If your intention is somehow to do a status change/check this might no be a good logic as you have seen setState is not synchronous. Do this carefully.

Categories