Hi I am trying to make the new Apollo Mutation component which was released in v2.1 work w/ react-final-form. It goes along pretty well so far. But now I finally dealing with server errors. It seems like in Apollo I can only access server errors in the onError callback and not within final-form onSubmit itself. Is there a way to populate submitErrors from within onError?
In other words how to combine Apollo 2.1 + react-final-form the right way?
<Mutation
mutation={REGISTER_USER}
onCompleted={result => {
console.log('result', result);
}}
onError={({ graphQLErrors, networkError }) => {
console.log('graphQLErrors', graphQLErrors[0].message);
// {'email': 'Email address is invalid.'}
}}
>
{registerUser => (
<FinalForm
onSubmit={values => {
registerUser({
variables: values,
});
}}
initialValues={currentUser}
render={({
handleSubmit,
pristine,
invalid,
}: FormRenderProps) => (
<form onSubmit={handleSubmit}>
<UsernameField externalLoading={loading} />
<EmailField />
<LanguageField />
<SubmitButton
initial
pristine={pristine}
invalid={invalid}
>
Register User
</SubmitButton>
</form>
)}
/>
)}
</Mutation>
You guys are gonna make me learn Apollo... 馃槃
Maybe look at @kavink's work here https://github.com/final-form/react-final-form/issues/23#issuecomment-375581107?
I have the same issue. I am using ApolloConsumer as workaround.
I think you can use ApolloConsumer instead of Mutation to do the manual mutation.
I found a solution so this is how I do it, namely using Apollo Higher Order Components.
import {
Field,
Form as FinalForm,
type FormRenderProps,
} from 'react-final-form';
import { compose, graphql, withApollo } from 'react-apollo';
const CompleteRegistrationForm = compose(
graphql(GET_INITIAL_VALUES),
graphql(COMPLETE_REGISTRATION),
// withApollo
// you only need this hoc if you want to get access to apollo client
// that would make sense if you would have to update the cache in onSubmit
Form => ({ data, mutate, client, ...rest }) => {
return (
<Form
{...rest}
initialValues={data.initialValues}
onSubmit={values => {
console.log('values', values);
return mutate({ variables: values })
.then(({ data }) => {
// do something with the data returned from a successful mutation
})
.catch(({ graphQLErrors }) => {
// prepare GraphQL errors, massage the error such that is has the react-final-form format for submission errors and finally return it so be captured as submission errors in the form
console.log('graphQLErrors', graphQLErrors);
return graphQLErrors.length && JSON.parse(graphQLErrors[0].message);
});
}}
render={({ handleSubmit, pristine, invalid }: FormRenderProps) => (
<form onSubmit={handleSubmit}>
<UsernameField />
<EmailField />
<LanguageField />
<Button initial pristine={pristine} invalid={invalid}>
Complete registration
</Button>
</form>
)}
/>
);
},
)(FinalForm);
Conclusion: This works pretty well. In this example we have two things going on: 1) A query to obtain initial values to populate the form with as well as a mutation in the form of a promise where you could chain .then and .catch on it. That was not possible using the Mutation component from apollo-react directly. Well it is possible, but it is not possible to get the result of the mutation inside the onSubmit callback. Instead if I had used the Mutation component, I would have gotten access to the result object in the onCompleted and onError callbacks of the Mutation component. But from over there you cannot populate submitErrors. Overall, this looks more readable and comprehensible. On the flip side it means some of the components will be labeled Unknown in the React debugging tools. Maybe someone has a better solution, but this works for me.
@soosap Any chance you could whip up a sandbox example I could publicize on the readme?
@soosap
I think your way is similar to using ApolloConsumer in latest version of react-apollo.
ApolloConsumer allow you to get the ApolloClient with renderProps way.
I am not big friend of HOC, use ApolloConsumer can achieve the same result.
The code would roughly like this.
<ApolloConsumer>
{(client) => (
<Form
onSubmit={async values => {
try {
await client.mutate({ mutation: MUTATION, variables: values });
} catch (e) {
.....
}
}}
....
/>
)}
</ApolloConsumer>
This is the way I am doing it now. It works for me. Looking forward for your feedback:
const RegistrationFinalForm = ({ history }) => {
return (
<Mutation mutation={REGISTER_USER}
onCompleted={(data) => {
history.push("/check-email");
}}>
{(register, { data, client }) => (
<div>
<Form
onSubmit={(values) => {
register({
variables: {
input: {
username: values.username,
email: values.email,
password: values.password
}
}
});
}}
validate={values => {
const errors = {};
if (!values.username) {
errors.username = "Required";
}
if (!values.email) {
errors.email = "Required";
}
if (!values.password) {
errors.password = "Required";
}
return errors;
}}
render={({ handleSubmit, pristine, invalid, values, variables, validate, meta }) => (
<form onSubmit={handleSubmit}
>
<Field name="email">
{({ input, meta }) => (
<div>
<label>Email</label>
<input {...input} type="text" placeholder="Email" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="username">
{({ input, meta }) => (
<div>
<label>Username</label>
<input {...input} type="text" placeholder="Username" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
<Field name="password">
{({ input, meta }) => (
<div>
<label>Password</label>
<input {...input} type="password" placeholder="Password" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
<button type="submit" >Submit</button>
</form>
)}
/>
</div>
)}
</Mutation>
);
};
@schingeldi Hey that's a pretty good way to do it. I do that too for simple forms. When it comes to more complex use cases I opt for Higher Order Components.
@erikras I wrote a blog post about this topic. It's the first one on our new blog hehe. Feel free to provide feedback and I update the article accordingly:) If you think it's good enough feel free to list this in your readme.
https://react-stuttgart.de/blog/using-react-final-form-with-apollo
@soosap: Thanks for the blogpost. I will study it soon. Do you have any solution on how to pass final-form-reacts [FORM_ERROR] back via GraphQL. I mean the one for form wide submitErrors
Hey, I think I have a solution. It might be a bit heavy, but I decided to abstract error massaging and prepping into a form utility function that I call handleSubmissionErrors.
/* @flow */
import { FORM_ERROR } from 'final-form';
type Props = {
errors: {
graphQLErrors: Array<{ message: string }>,
},
onCompleted?: () => void
};
export const handleSubmissionErrors = ({ errors, onCompleted }: Props) => {
// 1. Stop loading spinner or whatever onCompleted task is passed.
onCompleted && onCompleted();
// 2. Populate form submission errors
const { graphQLErrors } = errors;
const submissionErrors =
(graphQLErrors && JSON.parse(graphQLErrors[0].message)) || {};
if (submissionErrors['FORM_ERROR']) {
submissionErrors[FORM_ERROR] = submissionErrors['FORM_ERROR'];
delete submissionErrors['FORM_ERROR'];
}
return submissionErrors;
};
Then in the mutation .catch block within the onSubmit callback I call that function like so:
.catch(errors =>
handleSubmissionErrors({ errors, onCompleted: toggleDimmer }),
);
The server returns something like this:
{
"password": "Password is not secure.",
"FORM_ERROR": "Something wrong w/ your data."
}
Doing so populates submitError if form related errors are returned and likewise it populates submitErrors when field related errors are returned.
For me it was important that the error handling does not take up too much lines of code in the form component itself, the form logic becomes harder to read and maintain. Also this error handling logic is equivalent across all forms, so I thought a helper function is the best solution. Due to the fact that FORM_ERROR thing is a Symbol it is not trivial (or even possible?) to mimic that key server-side. Hope it helps - up for discussion of course. Let me know if you have a better solution.
Just to add a possible solution to this. I just pass as a prop the second argument from the Mutation render props. That way you'll have it as an attribute on the renderForm function
render(){
<Mutation mutation={SIGNUP_USER}>
{(signupUser, result) => (
<Form
onSubmit={this.onSubmit(signupUser)}
render={this.renderForm}
gqlResult={result} // Pass the arg as a prop
/>
)}
</Mutation>
}
// Then use it in the render Form
renderForm = form => {
console.log(form.gqlResult) // Here it is with errors and all
return ()
}
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
You guys are gonna make me learn Apollo... 馃槃
Maybe look at @kavink's work here https://github.com/final-form/react-final-form/issues/23#issuecomment-375581107?