React Query - How to find out which mutation led to refetching of query - react-query

In my simple ToDo app i am using useQuery() to fetch ToDo's from the server as well as useMutation() to create, update and delete ToDo's. When a mutation is successful, i invalidate the query so it gets refetched.
If i press the delete button of a ToDo item, i want the corresponding button to show a loading spinner until the mutation is done and the new ToDo's have been fetched. For that purpose i am using the useIsFetching() hook in my components, which works fine. However, here is the problem:
If i now execute a mutation, every button (meaning the "Delete" button as well as the "Submit" and "Save changes" button to post or update a ToDo) will show the loading spinner instead of just the one that i pressed. This makes sense since they all depend on the same value of useIsFetching(). I need a way to figure out which mutation led to the refetching of the query so i can conditionally render the loading spinner for the appropriate button. This seems to be a very common problem for me yet i cannot find a (not overcomplicated) solution for it. Is there something i'm missing?

The solution Ahmed Sbai said above is good (you can use state instead of local variables), and here is another approach for you.
You can check condition based on isLoading in the object returned from useMutation().
Updated: As written in this TkDodo's blog, the "magic" is here:
If you want your mutation to stay in loading state while your related queries update, you have to return the result of invalidateQueries() from the callback.
Therefore, you won't need to use the useIsFetching() hook, too.
function App() {
const addMutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
const updateMutation = useMutation({
mutationFn: (id, data) => {
return axios.patch(`/todos/${id}`, data)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
const deleteMutation = useMutation({
mutationFn: (id) => {
return axios.delete(`/todos/${id}`)
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ['todos'],
})
}
})
return (
<div>
{/* ... */}
<button
onClick={() => addMutation.mutate(...)}
loading={addMutation.isLoading}
>
Submit
</button>
<button
onClick={() => updateMutation.mutate(...)}
loading={updateMutation.isLoading}
>
Save changes
</button>
<button
onClick={() => deleteMutation.mutate(...)}
loading={deleteMutation.isLoading}
>
Delete
</button>
</div>
)
}
If you want any further information, please read more in Docs.

You can simply create a variable var loadingType = 0 and update its value each time the user click on a button for example if the delete button is clicked then loadingType = 1 if update button loadingType = 2, etc. Then based on the value of loadingType you know which loading spinner you have to use.

Related

How can I safely manipulate DOM in a StencilJS component?

I'm trying to safely remove a DOM node from a component made whit StencilJS.
I've put the removing code in a public method - It's what I need.
But, depending on which moment this method is called, I have a problem. If it is called too early, it don't have the DOM node reference yet - it's undefined.
The code below shows the component code (using StencilJS) and the HTML page.
Calling alert.dismiss() in page script is problematic. Calling the same method clicking the button works fine.
There is a safe way to do this remove()? Do StencilJS provide some resource, something I should test or I should wait?
import {
Component,
Element,
h,
Method
} from '#stencil/core';
#Component({
tag: 'my-alert',
scoped: true
})
export class Alert {
// Reference to dismiss button
dismissButton: HTMLButtonElement;
/**
* StencilJS lifecycle methods
*/
componentDidLoad() {
// Dismiss button click handler
this.dismissButton.addEventListener('click', () => this.dismiss());
}
// If this method is called from "click" event (handler above), everything is ok.
// If it is called from a script executed on the page, this.dismissButton may be undefined.
#Method()
async dismiss() {
// Remove button from DOM
// ** But this.dismissButton is undefined before `render` **
this.dismissButton.remove();
}
render() {
return (
<div>
<slot/>
<button ref={el => this.dismissButton = el as HTMLButtonElement} >
Dismiss
</button>
</div>
);
}
}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<title>App</title>
</head>
<body>
<my-alert>Can be dismissed.</my-alert>
<script type="module">
import { defineCustomElements } from './node_modules/my-alert/alert.js';
defineCustomElements();
(async () => {
await customElements.whenDefined('my-alert');
let alert = document.querySelector('my-alert');
// ** Throw an error, because `this.dismissButton`
// is undefined at this moment.
await alert.dismiss();
})();
</script>
</body>
</html>
There are multiple ways to delete DOM nodes in Stencil.
The simplest is to just call remove() on the element, like any other element:
document.querySelector('my-alert').remove();
Another would be to have a parent container that manages the my-alert message(s). This is especially useful for things like notifications.
#Component({...})
class MyAlertManager {
#Prop({ mutable: true }) alerts = ['alert 1'];
removeAlert(alert: string) {
const index = this.alerts.indexOf(alert);
this.alerts = [
...this.alerts.slice(0, index),
...this.alerts.slice(index + 1, 0),
];
}
render() {
return (
<Host>
{this.alerts.map(alert => <my-alert text={alert} />)}
</Host>
);
}
}
There are other options and which one to choose will depend on the exact use case.
Update
In your specific case I would just render the dismiss button conditionally:
export class Alert {
#State() shouldRenderDismissButton = true;
#Method()
async dismiss() {
this.shouldRenderDismissButton = false;
}
render() {
return (
<div>
<slot/>
{this.shouldRenderDismissButton && <button onClick={() => this.dismiss()}>
Dismiss
</button>
</div>
);
}
}
Generally I would not recommend manually manipulating the DOM in Stencil components directly since that could lead to problems with the next renders since the virtual DOM is out of sync with the real DOM.
And if you really need to wait for the component to render you can use a Promise:
class Alert {
loadPromiseResolve;
loadPromise = new Promise(resolve => this.loadPromiseResolve = resolve);
#Method()
async dismiss() {
// Wait for load
await this.loadPromise;
// Remove button from DOM
this.dismissButton.remove();
}
componentDidLoad() {
this.loadPromiseResolve();
}
}
I previously asked a question about waiting for the next render which would make this a bit cleaner but I don't think it's easily possible at the moment. I might create a feature request for this in the future.

Custom callBack on a Modal with conditional rendering

I am building a ride sharing app to learn react-native. In order to publish a new Ride, I have a Modal which renders conditionaly on the step state and each step has a diferente UI. Like:
let screen;
if (step===1){
ecraStep=<Screen1/>
} if (step===2){
ecraStep=<Screen2/>
} ...
On step=1 (which is the initial value) I want the callBack button to close the Modal and whenever step>1 I want it to call the following function:
function togglePreviousStep() {
setStep(step-1);
};
Which is essentially going back to the last rendered screen. I have tried it by writting this inside the Modal function component:
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
if (step>1) {
togglePreviousStep();
return true;
} else if (step===1) {
props.closeModal();
return false;
}
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [step, togglePreviousStep])
);
However, no matter the step state, whenever I press the backButton it closes the Modal. I don't understand what I am doing wrong.
EDITED
I implemented the Modal from react-native-modal. I used the prop onBackButtonPress like this:
<Modal
onBackButtonPress={props.onBackButtonPress}
visible={showModal}
//...
>
<NewRidesModal
//...
/>
</Modal>
And inside the Modal Screen I wrote:
if (step===1) {
onBackPressButton=(() => props.closeModal());
} else if (step>1){
onBackPressButton=(() => togglePreviousStep())
}
However, it still closes the modal when I press the android back button...
The onBackBackButtonPress is actually deprecated or removed.
Later on, I read a bit more about the modal documents on https://reactnative.dev/docs/modal#onrequestclose and I found out that:
The onRequestClose callback is called when the user taps the hardware
back button on Android or the menu button on Apple TV.
I should have investigated this before making this question. All I needed can be done with the onRequestClose prop like the following:
<Modal
onRequestClose={() => {
if (step===1) {
toggleModal();
} else if (step>1 && step<8){
togglePreviousStep();
}
}}
>
//...
</Modal>
This should work. If not put all your code involved because the split piece of them is hard to connect what are you doing.
<Modal
onBackButtonPress={() => {
if (step===1) {
props.closeModal();
} else if (step>1){
togglePreviousStep()
}
}}
visible={showModal}
//...
>
<NewRidesModal
//...
/>
</Modal>

react-bootstrap-typeahead How do I trigger a callback (update state) on `Enter` but not when the user is selecting hint?

I'm trying to make an input that will allow user to select multiple items. When finished selecting, he can press Enter to push his selections into an array in the state. The input will then clear and be ready for his next set of selections. (Imagine populating a 2-d array quickly). However, with the code below, if the user presses Enter to select the hint, my callback, pushToState, will trigger as it's in onKeyDown. What's the correct way to implement this behavior?
<Typeahead
id="basic-typeahead-multiple"
labelKey="name"
multiple
onChange={setMultiSelections}
options={options}
selected={multiSelections}
onKeyDown={(event) => {
if (event.key === 'Enter') {
pushToState(multiSelections);
setMultiSelections([]);
}
}}
/>
Your use case is similar to the one in this question. You basically need to determine whether the user is selecting a menu item or not. You can do that by checking the activeIndex of the typeahead:
// Track the index of the highlighted menu item.
const [activeIndex, setActiveIndex] = useState(-1);
const onKeyDown = useCallback(
(e) => {
// Check whether the 'enter' key was pressed, and also make sure that
// no menu items are highlighted.
if (event.key === 'Enter' && activeIndex === -1) {
pushToState(multiSelections);
setMultiSelections([]);
}
},
[activeIndex]
);
return (
<Typeahead
id="basic-typeahead-multiple"
labelKey="name"
multiple
onChange={setMultiSelections}
options={options}
onChange={setMultiSelections}
onKeyDown={onKeyDown}
selected={multiSelections} >
{(state) => {
// Passing a child render function to the component exposes partial
// internal state, including the index of the highlighted menu item.
setActiveIndex(state.activeIndex);
}}
</Typeahead>
);

Leaflet - How to add click event to button inside marker pop up in ionic app?

I am trying to add a click listener to a button in a leaftlet popup in my ionic app.
Here I am creating the map & displaying markers, also the method I want called when the header tag is clicked is also below:
makeCapitalMarkers(map: L.map): void {
let eventHandlerAssigned = false;
this.http.get(this.capitals).subscribe((res: any) => {
for (const c of res.features) {
const lat = c.geometry.coordinates[0];
const lon = c.geometry.coordinates[1];
let marker = L.marker([lon, lat]).bindPopup(`
<h4 class="link">Click me!</h4>
`);
marker.addTo(map);
}
});
map.on('popupopen', function () {
console.log('Popup Open')
if (!eventHandlerAssigned && document.querySelector('.link')) {
console.log('Inside if')
const link = document.querySelector('.link')
link.addEventListener('click', this.buttonClicked())
eventHandlerAssigned = true
}
})
}
buttonClicked(event) {
console.log('EXECUTED');
}
When I click this header, Popup Open & Inside if are printed in the console, so I know I'm getting inside the If statement, but for some reason the buttonClicked() function isn't being executed.
Can someone please tell me why this is the current behaviour?
I just ran into this issue like 2 hours ago. I'm not familiar with ionic, but hopefully this will help.
Create a variable that keeps track of whether or not the content of your popup has an event handler attached to it already. Then you can add an event listener to the map to listen for a popup to open with map.on('popupopen', function(){}). When that happens, the DOM content in the popup is rendered and available to grab with a querySelector or getElementById. So you can target that, and add an event listener to it. You'll have to also create an event for map.on('popupclose', () => {}), and inside that, remove the event listener from the dom node that you had attached it to.
You'd need to do this for every unique popup you create whose content you want to add an event listener to. But perhaps you can build a function that will do that for you. Here's an example:
const someMarker = L.marker(map.getCenter()).bindPopup(`
<h4 class="norwayLink">To Norway!</h4>
`)
someMarker.addTo(map)
function flyToNorway(){
map.flyTo([
47.57652571374621,
-27.333984375
],3,{animate: true, duration: 5})
someMarker.closePopup()
}
let eventHandlerAssigned = false
map.on('popupopen', function(){
if (!eventHandlerAssigned && document.querySelector('.norwayLink')){
const link = document.querySelector('.norwayLink')
link.addEventListener('click', flyToNorway)
eventHandlerAssigned = true
}
})
map.on('popupclose', function(){
document.querySelector('.norwayLink').removeEventListener('click', flyToNorway)
eventHandlerAssigned = false
})
This is how I targeted the popup content and added a link to it in the demo for my plugin.
So yes you can't do (click) event binding by just adding static HTML. One way to achieve what you want can be by adding listeners after this new dom element is added, see pseudo-code below:
makeCapitalMarkers(map: L.map): void {
marker.bindPopup(this.popUpService.makeCapitalPopup(c));
marker.addTo(map);
addListener();
}
makeCapitalPopup(data: any): string {
return `` +
`<div>Name: John</div>` +
`<div>Address: 5 ....</div>` +
`<br/><button id="myButton" type="button" class="btn btn-primary" >Click me!</button>`
}
addListener() {
document.getElementById('myButton').addEventListener('click', onClickMethod
}
Ideally with Angular, we should not directly be working with DOM, so if this approach above works you can refactor adding event listener via Renderer.
Also I am not familiar with Leaflet library - but for the above approach to work you need to account for any async methods (if any), so that you were calling getElementById only after such DOM element was successfully added to the DOM.

backgrid.js - how to prevent multi-row selection?

I am new to backgrid and using it in a form to allow the user to select a row (via a checkbox) and then click "Submit". I cannot figure out how to configure my grid to behave like "radio buttons" in that only one row can be selected. Is this something backgrid natively supports or do I need to write a handler to "unselect" previously selected rows?
Here is a quick-n-dirty method:
wellCollection.on('backgrid:selected', function(model, selected) {
if (wellGrid.getSelectedModels().length > 1) {
model.trigger("backgrid:select", model, false);
alert('Only one selection is allowed.');
}
});
The downside is this approach requires the use of "SelectAll" which is really counter-intuitive to the user. I would prefer to be able to not use "SelectAll" but it is required to get the getSelectedModels object populated.
You could create a custom cell that renders radio buttons. The implementation below may need some more work but something like this will get you started:
var RadioCell = Backgrid.Cell.extend({
events: {
"click .selectedRadio": "makeSelected"
},
render: function () {
this.template = Mustache.to_html($("#radioCell").html(), this.model.toJSON());
this.$el.html(this.template);
this.delegateEvents();
return this;
},
makeSelected: function () {
// set all to inactive
this.model.collection.invoke('set', { "SomeModelAttrib": false });
// check if radio is checked and set the value on the model
var radioValue = $("input[name='someSelection']").is(":checked");
this.model.set("SomeModelAttrib", radioValue);
}
});
and the mustache template:
<script type="text/template" id="radioCell">
<input type='radio' class='selectedRadio' name='someSelection' value="{{SomeModelAttrib}}" {{#SomeModelAttrib}}checked{{/SomeModelAttrib}}>
</script>