I'm working on a project using react-query where I'm displaying a list. Each entry in the list consists of multiple input fields and it should be able to save just a single entry as well as possible to save all the entries at once.
While saving the data I want to display loading indicators for the elements that are being saved a retry button in case the saving fails and a success message when it works.
I see it's possible to get ahold of the MutationCache, but I can't seem to find anything about triggering mutations from outside the component where it's used.
I made a small codesandbox to illustrate the setup, otherwise my code is pasted below.
https://codesandbox.io/s/react-query-forked-5cuxgb?file=/src/Form.jsx
Form.js
import * as React from "react";
import { Person } from "./Person";
export const Form = () => {
const people = [
{
id: 1,
name: "John Doe",
age: 37
},
{
id: 2,
name: "Jack Johnson",
age: 45
},
{
id: 3,
name: "Jimmie Jones",
age: 23
}
];
const saveAll = () => {
// Trigger mutations here?
};
return (
<div>
{people.map((person) => (
<Person key={person.id} {...person} />
))}
<hr />
<button onClick={saveAll}>Save all</button>
</div>
);
};
Person.js
import * as React from "react";
import { useCreatePersonMutation } from "./useCreatePersonMutation";
export const Person = (props) => {
const { mutate, status } = useCreatePersonMutation(props.id);
return (
<div>
{status === "loading" && <span>Saving...</span>}
{status === "success" && <span>Success</span>}
{status === "error" && (
<button onClick={mutate} style={{ marginRight: 12 }}>
Retry
</button>
)}
{status === "idle" && (
<button onClick={mutate} style={{ marginRight: 12 }}>
Create Person
</button>
)}
<input value={props.name} disabled={status === "loading"} />
<input value={props.age} disabled={status === "loading"} />
</div>
);
};
useCreatePersonMutation
import { useMutation } from "react-query";
export const useCreatePersonMutation = (id) => {
return useMutation({
mutationKey: ["Create_Person", id],
mutationFn: () => new Promise((resolve) => setTimeout(resolve, 3000))
});
};
You can't really go into the mutation cache (queryClient.getMutationCache()) and look for existing mutations and invoke them, because mutations only "exist" once they have been invoked with .mutate or .mutateAsync.
So the mutations in your component aren't really "there yet".
The easiest solution would imo be to:
have a separate mutation that lives in the Form component
you invoke all requests in there in parallel to create all users
this will give you a separate loading state that you can either pass down to all components, or you just add one extra loading state (overlay) to the whole form while this mutation is running.
I ended up achieving the desired behaviour by executing a mutation for each person being saved.
const saveAll = () => {
Promise.allSettled(people.map((person) => mutateAsync(person)));
};
In the Person component that renders each row, I listen to the mutation cache and try to find the matching mutation by comparing the persons name with the name being passed to the mutation.
React.useEffect(() => {
queryClient.getMutationCache().subscribe((listener) => {
if (!listener) return;
if (listener.options?.variables.name !== name) return;
setStatus(listener.state.status);
});
}, [queryClient, name]);
This allows each Person component to show the status of the mutation. And retrying a mutation is as simple as executing the mutation.
const retry = () => {
const mutation = queryClient.getMutationCache().find({
predicate: (mutation) => mutation.options.variables.name === name
});
if (mutation) {
mutation.execute();
}
};
It doesn't scale well performance wise if you work with large lists, since each Person component gets notified about each and every mutation that gets triggered.
However the lists I work with are of limited size, so for now it seems to suit my needs.
https://codesandbox.io/s/react-query-forked-5cuxgb
Related
So I am creating an app that utilizes the REST countries API and I am trying to call to the API on the first render and from my understanding, in order to do this you have to use an empty array in the useEffect function as such
const LightMode = () => {
const data = useRef([])
useEffect(() =>{
axios.get('https://restcountries.com/v3.1/all').then(res=>{
res.data.forEach(country =>{
//console.log(country)
data.current.push({
name: country.name.common,
population: country.population,
region: country.region,
capital: country.capital,
image: country.coatOfArms.png
})
})
})
}, [])
console.log(data)
return(
<div>
<NavigationBar />
<div className='temp'>
<Card className='country-cards'>
<Card.Img variant='top' src={data[0].image}/>
<Card.Body>
<Card.Title></Card.Title>
</Card.Body>
</Card>
</div>
</div>
)
}
but when I run the app I get an error saying unable to read undefined so the first render technically never runs? I want to know why that is. I am still learning how first renders work so any help is much appreciated, also if there is any more information needed let me know.
Error:
There are two issues at play here:
You are using Ref, which when updates will not trigger a re-render, which means once your REST call returns, the component will not re-render with your new data. Try using state instead of ref.
useEffect does run on the first render, but it is not blocking the render. Meaning, on the first render, useEffect is triggered, but it does not wait for the REST call to return before rendering the component. There are several methods to deal with it:
you can return null of there are no items in the array
you can use optional chaining (as the answer above suggests)
you can display a loading screen if there are no items in the array
and many more...
You are combining two things:
When useEffect is Run
Receiving a Response from the server
To actually test out whether useEffect ran, you can:
Add a console.log before that request and see if it got logged
Inspect your Network Tab to see if a Request to the Endpoint was triggered
Since your UI is dependent upon the response, it will be a good thing to add a:
Loader
Fallback component in case the response received is not valid
useEffect is supposed to run on every render unless a dependency array is provided, which will ensure that it only runs if a dependency changes.
By providing an empty array, you are limiting useEffect to run on JUST the first render.
use state instead of ref with async API call.
import axios from "axios";
import { useEffect, useState } from "react";
const LightMode = () => {
const [country, setCountry] = useState([]);
useEffect(() => {
const getCountries = async () => {
const countries = await axios.get("https://restcountries.com/v3.1/all");
const filterCountries = countries.data.map((country) => {
return {
name: country.name.common,
population: country.population,
region: country.region,
capital: country.capital,
image: country.coatOfArms.png
};
});
setCountry(filterCountries);
};
getCountries();
}, []);
return (
<div>
<NavigationBar />
{country.map((country) => (
<>
<div className="temp">
<Card className="country-cards">
<Card.Img variant="top" src={country.image} />
<Card.Body>
<Card.Title></Card.Title>
</Card.Body>
</Card>
</div>
</>
))}
</div>
);
};
export default LightMode;
At here, I use new_contries to save country when i fillter country(name, population, region, capital, image). After that, i SetContries (new_countries)
const LightMode = () => {
const [countries, setCountries] = useState([]);
useEffect(() => {
axios.get("https://restcountries.com/v3.1/all").then((res) => {
let new_countries: any = [];
res.data.forEach((country: any) => {
new_countries = [...new_countries, {
name: country.name.common,
population: country.population,
region: country.region,
capital: country.capital,
image: country.coatOfArms.png,
}];
});
setCountries(new_countries);
});
}, []);
return (
<div>
{countries?.map((country) => (
<>
<div>{country["name"]}</div>
</>
))}
</div>
);
}
Try adding optional chaining to your code:
<Card.Img variant='top' src={data[0]?.image}/>
I'm currently working on the checkout page of an application where a user can purchase up to three items at one of three prices chosen by the user (this is mostly being done as an experiment). When the user chooses a price by clicking a button this triggers a setState and a new price is stored to the state. When doing console.log I see the new state has been set, but upon checkout it appears the state resets to its initial value. I can't tell why and have no idea where to begin on this one. I imagine on initial render paypal is keeping the initial state it was passed and needs to be rerendered when the new state is set, but not sure how to go about this or even if this is the problem. Any help or guidance is appreciated.
I'm using the #paypal/react-paypal-js library for this paypal implementation, but am welcome to alternative suggestions.
Here is the code I'm using but cut down relevant sections:
import React, {useState, useRef, useEffect} from 'react';
import { PayPalButtons, usePayPalScriptReducer } from "#paypal/react-paypal-js";
import PriceButton from './PriceButton.jsx';
import NumberItemButton from './NumberItemButton';
import {priceOptions, amountItems} from './PriceOptions';
const PaymentPage = () => {
const [isLoading, setIsLoading] = useState(false);
const [payAmount, setPayAmount] = useState('5.00');
const [itemAmount, setItemAmount] = useState('1');
const payPalOptions = { //Note: This is used in the higher level component PayPalScriptProvider
"client-id": `${process.env.REACT_APP_PAYPAL_CLIENT_ID}`,
currency: "USD",
intent: "capture",
};
const createOrder = (data, actions) => { //This will show the initial state when triggered
return actions.order.create({
purchase_units : [
{
amount: {
value: payAmount //This stays at the initial State of '5.00' despite the newState being set
}
}
]
})
};
const onApprove = (data, actions) => {
return actions.order.capture().then(function(orderData) {
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
let transaction = orderData.purchase_units[0].payments.captures[0];
alert('Transaction '+ transaction.status + ': ' + transaction.id + '\n\nSee console for all available details');
}
)};
const onError = (error) => {
console.log(error)
}
console.log(payAmount) //Note: This will show the new State
return (
<div>
<h1>Purchase</h1>
<label> Choose number of items
<div>
{amountItems.map((item, index) => {
return <NumberItemButton key={index} setItemAmount={setItemAmount} amount={item.amount} />
})}
</div>
</label>
<label> Pick a price
<div>
{priceOptions.map((item, index) => {
return <PriceButton key={index} itemAmount={itemAmount} setPayAmount={setPayAmount} price={item.price} />
})}
</div>
</label>
<PayPalButtons
createOrder={(data, actions) => createOrder(data, actions)}
onApprove={(data, actions) => onApprove(data, actions)}
onError={onError}
/>
</div>
);
}
export default PaymentPage;
I'll also add the price button component incase the issue is there
const PriceButton = ({itemAmount, setPayAmount, price}) => { //itemAmount is the amount customer buys, price is the value passed through on the mapping function
const multPrice = (itemAmount * price).toFixed(2);
const withTaxPrice = (parseInt(multPrice) + .5).toFixed(2).toString();
return (
<button onClick={() => setPayAmount(withTaxPrice)}>${multPrice}</button>
)
}
export default PriceButton;
Appreciate any help!
I came back to this with a fresh pair of eyes and found the solution (though I'm not sure if it's the best one).
The issue is when the Paypal button renders it pulls in the initial state that is passed through, but it needs to be rerendered when a new state is passed.
My solution to this was to pass a forceReRender={[payAmount]} within the PaypalButtons component. This rerenders the Paypal button upon update to the price state and allows me to pass an updated value.
Hope this helps others!
I found a better solution. Just use useRef and access the ref.current value!
I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.
This is my first attempt to refactor code from a class component to a functional component using React hooks. The reason we're refactoring is that the component currently uses the soon-to-be-defunct componentWillReceiveProps lifecylcle method, and we haven't been able to make the other lifecycle methods work the way we want. For background, the original component had the aforementioned cWRP lifecycle method, a handleChange function, was using connect and mapStateToProps, and is linking to a repository of tableau dashboards via the tableau API. I am also breaking the component, which had four distinct features, into their own components. The code I'm having issues with is this:
const Parameter = (props) => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
let parameterSelections = parameterCurrent;
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [props.parameterCurrent])
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
console.log(parameterCurrent[key]);
return (
prevState => ({
...prevState,
parameterSelections: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
Swal.fire({
position: "center",
icon: "success",
title:
JSON.stringify(key) + " set to " + JSON.stringify(valKey),
font: "1em",
showConfirmButton: false,
timer: 2500,
heightAuto: false,
height: "20px"
});
})
.otherwise(function (err) {
alert(
Swal.fire({
position: "top-end",
icon: "error",
title: err,
showConfirmButton: false,
timer: 1500,
width: "16rem",
height: "5rem"
})
);
});
}
);
};
const classes = useStyles();
return (
<div>
{Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
console.log(parameterSelections[key])
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterSelections[key]}
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
}
</div >
)};
export default Parameter;
The classes const is defined separately, and all imports of reducers, etc. have been completed. parameterSelect in the code points to all available parameters, while parameterCurrent points to the default parameters chosen in the dashboard (i.e. what the viz initially loads with).
Two things are happening: 1. Everything loads fine on initial vizualization, and when I click on the Radio Button to change the parameter, I can see it update on the dashboard - however, it's not actually showing the radio button as being selected (it still shows whichever parameter the viz initialized with as being selected). 2. When I click outside of the Filterbar (where this component is imported to), I get Uncaught TypeError: func.apply is not a function. I refactored another component and didn't have this issue, and I can't seem to determine if I coded incorrectly in the useEffect hook, the handleParameterChange function, or somewhere in the return statement. Any help is greatly appreciated by this newbie!!!
This is a lot of code to take in without seeing the original class or having a code sandbox to load up. My initial thought is it might be your useEffect
In your refactored code, you tell your useEffect to only re-run when the props.parameterCurrent changes. However inside the useEffect you don't make use of props.parameterCurrent, you instead make use of parameterCurrent from the local lexical scope. General rule of thumb, any values used in the calculations inside a useEffect should be in the list of re-run dependencies.
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [parameterCurrent])
However, this useEffect doesn't seem to do anything, so while its dependency list is incorrect, I don't think it'll solve the problem you are describing.
I would look at your dispatch and selector. Double check that the redux store is being updated as expected, and that the new value is making it from the change callback, to the store, and back down without being lost due to improper nesting, bad key names, etc...
I'd recommend posting a CodeSandbox.io link or the original class for further help debugging.
The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};