In my component, I am using react-adopt, to compose graphql queries and mutations, so that my render props don't get too messy. I have the following code:
This is my mutation, it takes one argument - planID.
const CREATE_ORDER_MUTATION = gql`
mutation CREATE_ORDER_MUTATION($planID: String!) {
createOrder(planID: $planID) {
planID
name
description
subscriptionType
images
subscriptionType
pricePerMonth
}
}
This is the adopt function, it takes a couple of mutations, one of which is createOrder. The way Apollo works is that I need to pass variables prop to createOrder component here. The problem is, I don't have the planID at this point. planID is available only inside of the actual component.
const Composed = adopt({
toggleCart: <Mutation mutation={TOGGLE_CART_MUTATION} />,
createOrder: <Mutation mutation={CREATE_ORDER_MUTATION} />,
});
My component looks like this. I have the planID available here, but how can I pass it as an argument to mutation?!
render() {
const { plan } = this.props;
return (
<Composed>
{({ toggleCart, createOrder }) => {
const handleAdd = () => {
toggleCart();
Router.push('/waschingmachine');
createOrder();
};
return (
<StyledPlan>
<h1>{plan.name}</h1>
<p>{plan.description}</p>
<img src={`static${plan.images[0]}`} alt="blue1" />
<h2>{plan.pricePerMonth / 100} EUR</h2>
<div className="buttons">
<Link
href={{
pathname: '/plan',
query: { id: plan.id },
}}
>
<button type="button">info</button>
</Link>
<button onClick={handleAdd} type="button">
Select Plan
</button>
</div>
</StyledPlan>
);
}}
</Composed>
);
}
If there is no way to solve it this way, how would you approach it differently?
Here is how you can pass arguments through react-adopt way into your inner mutations mapper:
// In a nutshell, `react-adopt` allows you to pass props to your `Composed`
// component, so your inner `mapper` can access those props to pass them to
// your <Query> or <Mutation>
// Here's your initial `Composed` component from above
const Composed = adopt({
// `planId` is passed from below via props (see `<ContainerComponent />)
toggleCart: ({ planId, render }) => (
<Mutation mutation={TOGGLE_CART_MUTATION} variables={{ planId }}>{render}</Mutation>,
),
// `planId` is passed from below via props (see `<ContainerComponent />)
createOrder: ({ planId, render })=> (
<Mutation mutation={CREATE_ORDER_MUTATION} variables={{ planId }}>{render}</Mutation>
)
});
// `<ContainerComponent />` will take a plan as its props and passed `planId` to
// the `<Composed />` component
const ContainerComponent = ({ plan }) => (
<Composed planId={plan.id}>
{({ toggleCart, createOrder }) => {
const handleAdd = e => {
e.preventDefault();
toggleCart();
// ...
createOrder();
// ...
};
// Whatever your UI needs you can pass them here via here
return <YourPresentationComponent onClick={handleAdd} />;
}}
</Composed>
)
// Now all you need to do is to pass `plan` from your render() to the
// <ContainerComponent /> (This is the render() where you return your
render() {
const { plan } = this.props;
return <ContainerComponent plan={plan} />
}
Hopefully this can be helpful to solve your issue! Just a side note, you can also get the previous mapper value and pass them onto the next mapper fn as part of the argument, see below:
const Composed = adopt({
// Here you can retrieve the actual `plan` by using `userId` and pass the result
// into your next children mapper fn to consume
plan: ({ userId, render }) => (
<Query query={GET_PLAN_GRQPHQL} variables={{ userId }}>{render}</Query>
),
// `plan` is actually coming from above <Query /> component
toggleCart: ({ plan, render }) => { //... },
createOrder: ({ plan, render }) => { //...}
});
The mutate function passed in the rendered children can be called with options.
Options can include variables used in the GraphQL mutation string. [1].
This means that you can call the createOrder mutation function like so.
createOrder({ variables: { planID: 'some plan id' } });
Given the dynamic nature of planID, there are a number of ways to implement this. One of which is to use data attributes as below:
A data attribute can be set on for the plan id on the button .
<button onClick={handleAdd} data-planid={plan.id} type="button">
Select Plan
</button>
handleAdd can be refactored to get the planid from the target dataset attribute and invoke createOrder with planID variable.
const handleAdd = event => {
const planID = event.target.dataset.planid;
toggleCart();
Router.push('/waschingmachine');
createOrder({ variables: { planID } });
};
Another is to directly pass planID to handleAdd when calling it in the onClick prop for the button.
<button onClick={() => handleAdd(plan.id)} type="button">
Select Plan
</button>
Then update the handler
const handleAdd = planID => {
toggleCart();
Router.push('/waschingmachine');
createOrder({ variables: { planID } });
};
There are tradeoffs to both approaches. For the earlier approach, the planid are set in the DOM as attributes and can be read later.
While for the later one, N handlers are created for N plans and are kept in memory.
Related
I have a Material UI Autocomplete combo-box child component class that fetches results as the user types:
...
fetchIngredients(query) {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState({
options: options
});
});
}
...
<Autocomplete
options={this.state.options}
value={this.state.value}
onChange={(e, val) => {
this.setState({value: val});
}}
onInputChange={(event, newInputValue) => {
this.fetchIngredients(newInputValue);
}}
renderInput={(params) => {
// Hidden input so that FormData can find the value of this input.
return (<TextField {...params} label="Foo" required/>);
}}
// Required for search as you type implementations:
// https://mui.com/components/autocomplete/#search-as-you-type
filterOptions={(x) => x}
/>
...
This child component is actually rendered as one of many in a list by its parent. Now, say I want the parent component to be able to set the value of each autocomplete programmatically (e.g., to auto-populate a form). How would I go about this?
I understand I could lift the value state up to the parent component and pass it as a prop, but what about the this.state.options? In order to set a default value of the combo-box, I'd actually need to also pass a single set of options such that value is valid. This would mean moving the ajax stuff up to the parent component so that it can pass options as a prop. This is starting to get really messy as now the parent has to manage multiple sets of ajax state for a list of its Autocomplete children.
Any good ideas here? What am I missing? Thanks in advance.
If these are children components making up a form, then I would argue that hoisting the value state up to the parent component makes more sense, even if it does require work refactoring. This makes doing something with the filled-in values much easier and more organized.
Then in your parent component, you have something like this:
constructor(props) {
super(props);
this.state = {
values: [],
options: []
};
}
const fetchIngredients = (query, id) => {
this.sendAjax('/getOptions', {
data: {
q: query
}
}).then((options) => {
this.setState(prevState => {
...prevState,
[id]: options
});
});
}
const setValue = (newValue, id) => {
this.setState(prevState => {
...prevState,
[id]: newValue
};
}
render() {
return (
<>
...
{arrOfInputLabels.map((label, id) => (
<ChildComponent
id={id}
key={id}
value={this.state.values[id]}
options={this.state.options[id]}
fetchIngredients={fetchIngredients}
labelName={label}
/>
)}
...
</>
List of items to render
Given a list of items (coming from the server):
const itemsFromServer = {
"1": {
id: "1",
value: "test"
},
"2": {
id: "2",
value: "another row"
}
};
Function component for each item
We want to render each item, but only when necessary and something changes:
const Item = React.memo(function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
});
ItemList function component with a handleSave function that needs to be memoized.
And there is a possibility to save each individual item:
function ItemList() {
const [items, setItems] = useState(itemsFromServer);
const handleChange = useCallback(
function handleChange(id, value) {
setItems(currentItems => {
return {
...currentItems,
[id]: {
...currentItems[id],
value
}
};
});
},
[setItems]
);
async function handleSave(id) {
const item = items[id];
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
await save(item);
alert("Save done :)");
}
return (
<ul>
{Object.values(items).map(item => (
<Item
key={item.id}
id={item.id}
value={item.value}
onChange={handleChange}
onSave={handleSave}
/>
))}
</ul>
);
}
How to prevent unnecessary re-renders of each Item when only one item changes?
Currently on each render a new handleSave function is created. When using useCallback the items object is included in the dependency list.
Possible solutions
Pass value as parameter to handleSave, thus removing the items object from the dependency list of handleSave. In this example that would be a decent solution, but for multiple reasons it's not preferred in the real life scenario (eg. lots more parameters etc.).
Use a separate component ItemWrapper where the handleSave function can be memoized.
function ItemWrapper({ item, onChange, onSave }) {
const memoizedOnSave = useCallback(onSave, [item]);
return (
<Item
id={item.id}
value={item.value}
onChange={onChange}
onSave={memoizedOnSave}
/>
);
}
With the useRef() hook, on each change to items write it to the ref and read items from the ref inside the handleSave function.
Keep a variable idToSave in the state. Set this on save. Then trigger the save function with useEffect(() => { /* save */ }, [idToSave]). "Reactively".
Question
All of the solutions above seem not ideal to me. Are there any other ways to prevent creating a new handleSave function on each render for each Item, thus preventing unnecessary re-renders? If not, is there a preferred way to do this?
CodeSandbox: https://codesandbox.io/s/wonderful-tesla-9wcph?file=/src/App.js
The first question I'd like to ask : is it really a problem to re-render ?
You are right that react will re-call every render for every function you have here, but your DOM should not change that much it might not be a big deal.
If you have heavy calculation while rendering Item, then you can memoize the heavy calculations.
If you really want to optimize this code, I see different solutions here:
Simplest solution : change the ItemList to a class component, this way handleSave will be an instance method.
Use an external form library that should work fine: you have powerfull form libraries in final-form, formik or react-hook-form
Another external library : you can try recoiljs that has been build for this specific use-case
Wow this was fun! Hooks are very different then classes. I got it to work by changing your Item component.
const Item = React.memo(
function Item({ id, value, onChange, onSave }) {
console.log("render", id);
return (
<li>
<input
value={value}
onChange={event => onChange(id, event.target.value)}
/>
<button onClick={() => onSave(id)}>Save</button>
</li>
);
},
(prevProps, nextProps) => {
// console.log("PrevProps", prevProps);
// console.log("NextProps", nextProps);
return prevProps.value === nextProps.value;
}
);
By adding the second parameter to React.memo it only updates when the value prop changes. The docs here explain that this is the equivalent of shouldComponentUpdate in classes.
I am not an expert at Hooks so anyone who can confirm or deny my logic, please chime in and let me know but I think that the reason this needs to be done is because the two functions declared in the body of the ItemList component (handleChange and handleSave) are in fact changing on each render. So when the map is happening, it passes in new instances each time for handleChange and handleSave. The Item component detects them as changes and causes a render. By passing the second parameter you can control what the Item component is testing and only check for the value prop being different and ignore the onChange and onSave.
There might be a better Hooks way to do this but I am not sure how. I updated the code sample so you can see it working.
https://codesandbox.io/s/keen-roentgen-5f25f?file=/src/App.js
I've gained some new insights (thanks Dan), and I think I prefer something like this below. Sure it might look a bit complicated for such a simple hello world example, but for real world examples it might be a good fit.
Main changes:
Use a reducer + dispatch for keeping state. Not required, but to make it complete. Then we don't need useCallback for the onChange handler.
Pass down dispatch via context. Not required, but to make it complete. Otherwise just pass down dispatch.
Use an ItemWrapper (or Container) component. Adds an additional component to the tree, but provides value as the structure grows. It also reflects the situation we have: each item has a save functionality that requires the entire item. But the Item component itself does not. ItemWrapper might be seen as something like a save() provider in this scenario ItemWithSave.
To reflect a more real world scenario there is now also a "item is saving" state and the other id that's only used in the save() function.
The final code (also see: https://codesandbox.io/s/autumn-shape-k66wy?file=/src/App.js).
Intial state, items from server
const itemsFromServer = {
"1": {
id: "1",
otherIdForSavingOnly: "1-1",
value: "test",
isSaving: false
},
"2": {
id: "2",
otherIdForSavingOnly: "2-2",
value: "another row",
isSaving: false
}
};
A reducer to manage state
function reducer(currentItems, action) {
switch (action.type) {
case "SET_VALUE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
value: action.value
}
};
case "START_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: true
}
};
case "STOP_SAVE":
return {
...currentItems,
[action.id]: {
...currentItems[action.id],
isSaving: false
}
};
default:
throw new Error();
}
}
Our ItemList to render all items from the server
export default function ItemList() {
const [items, dispatch] = useReducer(reducer, itemsFromServer);
return (
<ItemListDispatch.Provider value={dispatch}>
<ul>
{Object.values(items).map(item => (
<ItemWrapper key={item.id} item={item} />
))}
</ul>
</ItemListDispatch.Provider>
);
}
The main solution ItemWrapper or ItemWithSave
function ItemWrapper({ item }) {
const dispatch = useContext(ItemListDispatch);
const handleSave = useCallback(
// Could be extracted entirely
async function save() {
if (item.value.length < 5) {
alert("Incorrect length.");
return;
}
dispatch({ type: "START_SAVE", id: item.id });
// Save to API
// eg. this will use otherId that's not necessary for the Item component
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: "STOP_SAVE", id: item.id });
},
[item, dispatch]
);
return (
<Item
id={item.id}
value={item.value}
isSaving={item.isSaving}
onSave={handleSave}
/>
);
}
Our Item
const Item = React.memo(function Item({ id, value, isSaving, onSave }) {
const dispatch = useContext(ItemListDispatch);
console.log("render", id);
if (isSaving) {
return <li>Saving...</li>;
}
function onChange(event) {
dispatch({ type: "SET_VALUE", id, value: event.target.value });
}
return (
<li>
<input value={value} onChange={onChange} />
<button onClick={onSave}>Save</button>
</li>
);
});
I am trying to do a wrapper for the graphQL queries, I tried this
const GQLWrapper = ({ query, children}) => (
<Query query={query}>
{({ loading, error, data }) => {
if (loading) {
return null
}
if (error) {
<QueryError />
}
const { gqlData } = data.page
return (
<div>
{children}
</div>
)
}}
</Query>
)
but i don't understand how to use render props to pass the data to the child component.
Also if it is a better solution to use HOC, please let me know (when should one be used or the other). Thank you
You just make your children a function and pass the data there
return <div>{children(gqlData)}</div>;
When you use your wrapper you do:
<GQLWrapper query={myquery}>
{(gqlData) => <SomeComponent data={gqlData} />}
</GQLWrapper>
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>
);
};
I'm trying to use the useMutation hook from react-apollo-hooks to execute a delete mutation, but I'm having difficulty passing the ID value of the post to the mutation hook in the following code:
const Posts = () => {
const { data, error, loading } = useQuery(GET_POST)
const onDeleteHandler = useMutation(DELETE_POST, {
variables: { id }
})
if (loading) return <div>...loading</div>
if (error) return <div>Error</div>
return data.posts.map(({id, title, body, location, published, author}) => {
return (
<div className="card" key={id}>
<p>id: {id}</p>
<p>title: {title}</p>
<p>body: {body}</p>
<p>location: {location}</p>
<p>published: {published}</p>
<p>author: {author.name}</p>
<Link to={`/post/${id}/edit`}>
Edit
</Link>
<button
onClick={onDeleteHandler}>
Delete
</button>
</div>
)
})
}
I cannot include the useMutation inside the onClick() property since a hook cannot be used as a callback function. I tried using const inputRef = useRef() and passing inputRef.current.value, but kept getting undefined.
From the react-apollo-hooks docs:
You can provide any mutation options as an argument to the useMutation hook or to the function returned by it
So you can omit the variables when calling useMutation:
const onDeleteHandler = useMutation(DELETE_POST)
and then pass them in when calling the handler:
onClick={() => onDeleteHandler({ variables: { id } })}>
Note: react-apollo-hooks is now deprecated since react-apollo now supports hooks. The API is a bit different, so usage would look like this:
const [onDeleteHandler, { data, loading, error }] = useMutation(DELETE_POST)
I guess you could do the same with useQuery().