react-router
react-router copied to clipboard
[Feature]: GraphQL Mutations in Actions
What is the new or updated feature that you are suggesting?
I would like to submit a request via GraphQL when I submit a form. I am using react-hook-form to validate and format the object I want to submit. I know the docs recommend using browser validation but I would prefer to keep react-hook-form if possible. There's an awful lot of stuff it handles for me. E.g. handling repeatable inputs, formatting all the fields into a structured object.
If I could pass an object into an action from my component via the useSubmit
hook, this would unlock a lot more scenarios for react-router
imo.
E.g. something like this:
import { API, graphqlOperation } from "aws-amplify";
import { useForm } from "react-hook-form";
import { useSubmit } from "react-router-dom";
import { createUser } from './graphql/mutations';
const CreateUser = () => {
const submit = useSubmit(); // react-router-dom
const { register, handleSubmit } = useForm(); // react-hook-form
const onSubmit = data => {
submit(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("user.name", { required: true })} />
<button type="submit">Save</button>
</form>
);
};
export default CreateUser;
export const action = async ({ request }) => {
console.log(request.user.name); // Request is the first object passed to submit();
await API.graphql(graphqlOperation(createUser, { input: request }));
};
Is there a better way to do this without such a feature? I can't see an obvious and nice way to invoke a graphql mutation without ditching react-form-hooks. This would mean I:
- Would need to build code to have repeatable inputs (adding a new item, removing an item, re-ordering etc)
- When the form is submitted to the action, I need to take the form data and map it into a JS object that matches the GraphQL mutation schema.
- Rollout my own validation code, using the browser validation APIs. I believe out of the box it shows errors straight away, whereas I would want to wait until the form is submitted once before showing errors. It is this kind of logic I don't want to rollout myself.
Maybe I shouldn't be using actions but my "OCD" is telling me that if I'm using loaders (which work great with GraphQL, there's docs for it), I should be using actions!
Why should this feature be included?
- Makes react-router less opinionated. Feels like the current design is really imposing on how developers should build forms, I don't think developers should have to leave their favoured form packages to use actions.
- Helps support graphql mutations which isn't done over form encoding.
You can submit a plain object (non-nested) and it will be serialized into the request form data:
let submit = useSubmit();
submit({ key: 'value' });
async function action({ request }) {
let fd = await request.formData()
// fd.get('key') === 'value'
}
Does that handle your use case?
Hi, thanks for replying and apolgies for getting back to you so late.
I didn't know about that feature so it's good to know as it helps a little. Knowing that I could do the following:
import { useForm } from "react-hook-form";
import { useSubmit } from "react-router-dom";
import { set } from "lodash";
export default function App() {
const { register, handleSubmit, formState: { errors } } = useForm();
const submit = useSubmit();
const onSubmit = data => submit(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name.first")} />
<input {...register("name.last", { required: true })} />
{errors.name.last && <span>This field is required</span>}
<input type="submit" />
</form>
);
};
export const action = async ({ request }) => {
const user = request.formData().entries().reduce((acc, [key, val]) => set(acc, key, val), {});
await API.graphql(graphqlOperation(createUser, { input: user }));
};
The action will accept the key/value formdata object and re-parse it back into a structured object. This approach has some flaws though, e.g. there's a difference in how react-form-hook handles arrays and how lodash set does: Lodash users[0].firstname
vs form hooks users.0.firstname
.
I think parsing the results in and out of formdata is not a nice approach. If it was possible to send a js object directly to the action that would make the scenario of using a graphql API and a form library much better.
We're pretty happy leveraging standardized web APIs such as FormData
, which is great for key/value pairs, including multiple values per key. If your object is complex such that a simple object with key/value pairs won't do the trick, then you can do any type of custom serialization you need and just send the string in a single-key object and then parse it in the action:
import { useForm } from "react-hook-form";
import { useSubmit } from "react-router-dom";
import { set } from "lodash";
export default function App() {
const { register, handleSubmit, formState: { errors } } = useForm();
const submit = useSubmit();
const onSubmit = data => submit({
// You can implement any custom serialization logic here
serialized: JSON.stringify(data)
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name.first")} />
<input {...register("name.last", { required: true })} />
{errors.name.last && <span>This field is required</span>}
<input type="submit" />
</form>
);
};
export const action = async ({ request }) => {
let formData = await request.formData();
// And then just parse your own format here
let user = JSON.parse(formData.get('serialized'));
await API.graphql(graphqlOperation(createUser, { input: user }));
};
@brophdawg11 Your solution is only a workaround as serialization is an unnecessary step if we can transfer a reference to an object.
It would be better to add the customData parameter to the SubmitFunction
export interface SubmitFunction {
(
/**
* Specifies the `<form>` to be submitted to the server, a specific
* `<button>` or `<input type="submit">` to use to submit the form, or some
* arbitrary data to submit.
*
* Note: When using a `<button>` its `name` and `value` will also be
* included in the form data that is submitted.
*/
target: SubmitTarget,
/**
* Options that override the `<form>`'s own attributes. Required when
* submitting arbitrary data without a backing `<form>`.
*/
options?: SubmitOptions
/**
* User data
*/
customData?: any): void;
}
and next pass customData as the third parameter to an action:
export interface ActionFunctionArgs extends DataFunctionArgs {
customData?: any;
}
Like @connelhooley I use graphql mutations (Relay), but my form outputs are typed objects obtained from Formik.
The new react-router version with loaders is fantastic, and it's great step forward as we all need render-as-you-fetch. However, the library needs some additional features to be truly useful.
We're pretty happy leveraging standardized web APIs such as
FormData
, which is great for key/value pairs, including multiple values per key. If your object is complex such that a simple object with key/value pairs won't do the trick, then you can do any type of custom serialization you need and just send the string in a single-key object and then parse it in the action:import { useForm } from "react-hook-form"; import { useSubmit } from "react-router-dom"; import { set } from "lodash"; export default function App() { const { register, handleSubmit, formState: { errors } } = useForm(); const submit = useSubmit(); const onSubmit = data => submit({ // You can implement any custom serialization logic here serialized: JSON.stringify(data) }); return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("name.first")} /> <input {...register("name.last", { required: true })} /> {errors.name.last && <span>This field is required</span>} <input type="submit" /> </form> ); }; export const action = async ({ request }) => { let formData = await request.formData(); // And then just parse your own format here let user = JSON.parse(formData.get('serialized')); await API.graphql(graphqlOperation(createUser, { input: user })); };
Could someone please tell why doesn't the example above work for me? Here's it is in a sandbox. https://codesandbox.io/s/nice-pike-zy3hqc?file=/src/index.js
I am content if I can just console.log the value inside the action. I can take it from there.
Thank you.
I figure it out. I had to use the fetcher to pass the data to action.
import { useForm } from "react-hook-form";
import { useFetcher } from "react-router-dom";
export default function App() {
const fetcher = useFetcher();
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) =>
fetcher.submit(
{
// You can implement any custom serialization logic here
serialized: JSON.stringify(data)
},
{ method: "post", action: "/" }
);
return (
<fetcher.Form onSubmit={handleSubmit(onSubmit)}>
<input {...register("first")} />
<input {...register("last", { required: true })} />
{errors?.name?.last && <span>This field is required</span>}
<input type="submit" />
</fetcher.Form>
);
}
export const action = async ({ request }) => {
console.log("action called....");
let formData = await request.formData();
// And then just parse your own format here
let user = JSON.parse(formData.get("serialized"));
console.log(user); // <== I am content if this console.log works
return null;
};
I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!