Fetch and use response to change state in React - reactjs

I would like to change the state of a component based on the response of a PUT request using react-refetch.
Especially when the response of the PUT is unsuccessful, as is the case with for example a 500 response.
The following example is an example in a form. When a user submits the form it should then fire off a PUT.
If the PUT response is fulfilled, it should reset the form. Otherwise nothing should happen, and the user should be able to retry.
./MyForm.jsx
import React from "react";
import PropTypes from "prop-types";
import { PromiseState } from "react-refetch";
import { Formik, Form, Field, ErrorMessage } from "formik";
import ResetOnSuccess from "./ResetOnSuccess";
const MyForm = ({ settingsPut, settingsPutResponse }) => {
const submitForm = (values, formik) => {
settingsPut(true);
// Here it should pick up the settingsPutResponse,
// and then do the following ONLY if it's successful:
//
// formik.resetForm({ values });
// window.scrollTo(0, 0);
};
return (
<div>
<Formik
noValidate
initialValues={{ name: "", password: "" }}
onSubmit={submitForm}
>
{({ dirty }) => (
<Form>
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
<Field type="text" name="name" />
<ErrorMessage name="name" component="div" />
<Field type="password" name="password" />
<ErrorMessage name="password" component="div" />
<button type="submit" disabled={dirty !== null ? !dirty : false}>
Submit
</button>
{settingsPutResponse && settingsPutResponse.rejected && (
<p style={{ color: "red" }}>Please try again</p>
)}
</Form>
)}
</Formik>
</div>
);
};
MyForm.propTypes = {
settingsPut: PropTypes.func.isRequired,
settingsPutResponse: PropTypes.instanceOf(PromiseState)
};
MyForm.defaultProps = {
userSettingsPutResponse: null
};
export default MyForm;
I might have a solution by creating a component:
./ResetOnSuccess.jsx
import React, { useEffect, useState } from "react";
import { useFormikContext } from "formik";
import PropTypes from "prop-types";
import { PromiseState } from "react-refetch";
const ResetOnSuccess = ({ settingsPutResponse }) => {
const { values, resetForm } = useFormikContext();
const [success, setSuccess] = useState(false);
useEffect(() => {
if (settingsPutResponse && settingsPutResponse.fulfilled) {
setSuccess(true);
}
}, [settingsPutResponse]);
// only if settingsPutResponse is fulfilled will it reset the form
if (success) {
resetForm({ values });
window.scrollTo(0, 0);
setSuccess(false);
}
return null;
};
ResetOnSuccess.propTypes = { settingsPutResponse: PropTypes.instanceOf(PromiseState) };
ResetOnSuccess.defaultProps = { settingsPutResponse: null };
export default ResetOnSuccess;
And then in ./MyForm.jsx add the reset component:
<Formik
noValidate
initialValues={{ name: "", password: "" }}
onSubmit={submitForm}
>
{({ dirty }) => (
<Form>
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
<Field type="text" name="name" />
<ErrorMessage name="name" component="div" />
<ResetOnSuccess settingsPutResponse={settingsPutResponse} />
// etc...
But since it's a component that returns a 'null'. This feels a bit like an anti-pattern.
Is there a better way?
I've created an codesandbox example here: https://codesandbox.io/s/quizzical-johnson-dberw

Related

Set props Data to context in formik

I have two components inside contact page. One is contact form and another is FormData where I'm showing what the user is typing in forms field. I also have a context where I want to store the form data. But in formik there are built-in props, from there I can access the values but from inside the form. But I want to pass the values outside the form to the context so that I can access this data from the FormData component.
Contact Form
import React, { useContext, useEffect } from "react";
import { useFormik } from "formik";
import { Formik } from "formik";
import { ContentContext } from "../Context";
import { FormData } from "./index";
const ContactForm = () => {
const [content, setContent] = useContext(ContentContext);
// useEffect(() => {
// setContent({
// ...content,
// contactFormData: props,
// });
// }, [props]);
// console.log(content);
return (
<Formik
initialValues={{ email: "" }}
onSubmit={async (values) => {
await new Promise((resolve) => setTimeout(resolve, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
// setContent({
// ...content,
// contactFormData: props,
// });
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email" style={{ display: "block" }}>
Email
</label>
<input
id="email"
placeholder="Enter your email"
type="text"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={
errors.email && touched.email
? "text-input error"
: "text-input"
}
/>
{errors.email && touched.email && (
<div className="input-feedback">{errors.email}</div>
)}
<button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</button>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
{/* <FormData props={props} />; */}
</form>
);
}}
</Formik>
);
};
export default ContactForm;
context
import React, { useState, createContext } from "react";
export const ContentContext = createContext();
export const ContentProvider = ({ children }) => {
const [content, setContent] = useState({
contactFormData: {
email: "",
},
});
return (
<ContentContext.Provider value={[content, setContent]}>
{children}
</ContentContext.Provider>
);
};
setting the context inside the form causes infinite loop. How do I save props to context?

React Labels not displaying in browser

I am new to React and especially formik. I am trying to learn how to create a login form using formik that will display a label called email with the email input. A label called password with the password input and a button called submit.
My problem is that only the two inputs and submit button displays in the browser. The two labels for email and password do not display in the browser. Please advise how I can fix this.
App.js:
import React from 'react';
import './App.css';
import FormikContainer from './components/FormikContainer';
import LoginForm from './components/LoginForm';
function App() {
return (
<div>
<LoginForm />
</div>
);
}
export default App;
LoginForm.js:
import React from 'react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import FormikContainer from './FormikContainer';
import FormikControl from './FormikControl';
function LoginForm() {
const initialValues = {
email: '',
password: ''
};
const validationschema = Yup.object({
email: Yup.string().email('invalid email format').required('Requird'),
password: Yup.string().required('Required')
});
const onSubmit = (values) => {
console.log('Form data', values);
};
return (
<div>
<Formik
initialValues={initialValues}
validationschema={validationschema}
onSubmit={onSubmit}
>
{(formik) => {
return (
<Form>
<FormikControl
control="input"
type="email"
label="Email"
name="email"
/>
<FormikControl
control="input"
type="password"
label="Password"
name="password"
/>
<button type="submit" disabled={!formik.isValid}>
Submit
</button>
</Form>
);
}}
</Formik>
</div>
);
}
export default LoginForm;
FormikControl.js:
import React from 'react';
function FormikControl(props) {
const { control, ...rest } = props;
switch (control) {
case 'input':
return <input {...rest} />;
case 'textarea':
case 'select':
case 'radio':
case 'checkbox':
case 'date':
default:
return null;
}
return <div></div>;
}
export default FormikControl;
FormikContainer.js
import React from 'react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import FormikControl from './FormikControl';
function FormikContainer() {
const initialValues = {
email: ''
};
const validationschema = Yup.object({
email: Yup.string().required('Required')
});
const onSubmit = (values) => console.log('Form data', values);
return (
<div>
<Formik
initialValues={initialValues}
validationschema={validationschema}
onSubmit={onSubmit}
>
{(formik) => (
<Form>
<FormikControl
control="input"
type="email"
label="Email"
name="email"
/>
<button type="submit">Submit</button>
</Form>
)}
</Formik>
</div>
);
}
export default FormikContainer;
I would like to do easily this instead :
function FormikControl({ control, id, label, ...rest }) {
return (
<>
{control === "input" && <label htmlFor={id}>{label}</label>}
<input id={id} {...rest} />
</>
);
}
export default FormikControl;

Formik catch-22

I'm new to React and I've run into a catch 22 situation when using Formik that I seem to have a mental block with. If I use withFormik() then my component can't use hooks in its submit handler.
import React, { useEffect } from "react";
import { Form, Field, withFormik } from "formik";
import { useDatabase, useAlerts } from "./Hooks";
const MyForm = props => {
const { resetForm, dirty, isSubmitting, setSubmitting } = props;
const { loadData, saveData } = useDatabase();
const { success } = useAlerts();
const reset = data => resetForm({ values: data });
useEffect(() => {
loadData().then(data => reset(data));
}, []);
// Problem: How can I execute this on submit?
const handleSubmit = async values => {
await saveData(values);
reset(values);
success("Values saved");
setSubmitting(false);
};
return (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field name="lastName" placeholder="Last name" readOnly={isSubmitting} />
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
);
};
export default withFormik({
mapPropsToValues: () => ({
firstName: "",
lastName: ""
}),
enableReinitialize: true,
handleSubmit: () => {
// Has no access to saveData() and success() hook methods
}
})(MyForm);
https://codesandbox.io/s/sleepy-blackburn-q1mt4?file=/src/MyForm.js
Alternatively if I don't use withFormik then I can't reset the form when my data has loaded because I don't have a reference to resetForm.
import React, { useEffect } from "react";
import { Form, Field, Formik } from "formik";
import { useDatabase, useAlerts } from "./Hooks";
const MyForm = props => {
const { loadData, saveData } = useDatabase();
const { success } = useAlerts();
// Problem: how can I reset the form on data load?
useEffect(() => {
loadData().then(data => resetForm({ values: data }));
}, []);
const initialValues = {
firstName: "",
lastName: ""
};
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await saveData(values);
resetForm({ values });
success("Values saved");
setSubmitting(false);
};
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ isSubmitting, dirty }) => (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field
name="lastName"
placeholder="Last name"
readOnly={isSubmitting}
/>
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
)}
</Formik>
);
};
export default MyForm;
https://codesandbox.io/s/hardcore-sound-048wf?file=/src/MyForm.js
What would be the best way to do this?
It seems that writing out the problem triggered some mind-cogs to start turning and I came up with a possible solution.
I introduced a context like this...
import React, { useState, useContext } from "react";
import { useDatabase } from "./Hooks";
export const DataContext = React.createContext(null);
export const DataContextConsumer = DataContext.Consumer;
export const useDataContext = () => useContext(DataContext);
export const DataContextProvider = props => {
const [data, setData] = useState({
firstName: "",
lastName: ""
});
const { loadData, saveData } = useDatabase();
const load = async () => setData(await loadData());
const save = async newData => await saveData(newData);
const contextValues = { load, save, data };
return (
<DataContext.Provider value={contextValues}>
{props.children}
</DataContext.Provider>
);
};
export const withDataContext = () => WrappedComponent => props => (
<DataContextProvider>
<WrappedComponent {...props} />
</DataContextProvider>
);
export default {
DataContext,
DataContextConsumer,
DataContextProvider,
useDataContext,
withDataContext
};
And then passed its data to Formik's initialValues. Now Formik gets the new values on load and I can call the save hooks in the submit hander.
import React, { useEffect } from "react";
import { Form, Field, Formik } from "formik";
import { withDataContext, useDataContext } from "./DataContext";
import { useAlerts } from "./Hooks";
const MyForm = () => {
const { load, save, data } = useDataContext();
const { success } = useAlerts();
useEffect(() => {
load();
}, []);
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await save(values);
resetForm({ values });
success("Values saved " + JSON.stringify(values));
setSubmitting(false);
};
return (
<Formik
initialValues={data}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{({ isSubmitting, dirty }) => (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field
name="lastName"
placeholder="Last name"
readOnly={isSubmitting}
/>
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
)}
</Formik>
);
};
export default withDataContext()(MyForm);
https://codesandbox.io/s/throbbing-cache-txnit?file=/src/MyForm.js
Perhaps this is a classic case of solving a React problem by lifting state.
You can lift the form inside the render prop for formik up into a new component allowing you to use hooks inside it. Now you can move the loadData effect into the lifted form and know that you are now defining the effect inside of both scopes where you can get access to resetForm via the render props and loadData via the useDatabase hook.
const LiftedForm = ({ isSubmitting, dirty, resetForm }) => {
const { loadData } = useDatabase();
useEffect(() => {
loadData().then((data) => resetForm({ values: data }));
}, [loadData, resetForm]);
return (
<Form>
<h1>Catch 22</h1>
<Field
name="firstName"
placeholder="First name"
readOnly={isSubmitting}
/>
<Field name="lastName" placeholder="Last name" readOnly={isSubmitting} />
<input disabled={!dirty} type="submit" />
<input type="reset" />
</Form>
);
};
and passing the formik bag via the render props right to the new lifted form component.
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
enableReinitialize={true}
>
{(props) => <LiftedForm {...props} />}
</Formik>
The handleSubmit needs no changing because it already receives everything it needs in the callback and has access to saveData and success via hooks.
const { saveData } = useDatabase();
const { success } = useAlerts();
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
await saveData(values);
resetForm({ values });
success("Values saved");
setSubmitting(false);
};
the codesandbox for that

Submitting a Formik Form on Unmount

Is it possible to submit a Formik Form without having a submit button?
For example submitting the Form when the component unmounts so the user doesn't need to click a button to save.
Think of something like that:
import React from "react";
import { Formik, Form, Field } from "formik";
const Example = () => {
useEffect(() => {
return () => {
//trigger Submit or send Request with Form Values from here
};
}, []);
return (
<Formik
initialValues={{
firstName: "",
lastName: "",
}}
onSubmit={(values, { setSubmitting }) => {
//send Request
}}
>
{() => (
<Form>
<Field name="firstName" />
<Field name="lastName" />
</Form>
)}
</Formik>
);
};
export default Example;
You can create a functional component that auto-submits. By having it render inside the component, you have reference to the context of the form. You can use:
import { useFormikContext } from 'formik';
function AutoSubmitToken() {
// Grab values and submitForm from context
const { values, submitForm } = useFormikContext();
React.useEffect(() => {
return submitForm;
}, [submitForm]);
return null;
};
You can use it under the component as such
<Form>
<Field name="token" type="tel" />
<AutoSubmitToken />
</Form>
You can read all about it here

React Hook Form with AntD Styling

I'm trying to figure out how to use react-hook-form with antd front end.
I have made this form and it seems to be working (it's part 1 of a multipart form wizard) except that the error messages do not display.
Can anyone see what I've done wrong in merging these two form systems?
I'm not getting any errors, but I think I have asked for both form fields to be required but if I press submit without completing them the error messages are not displayed.
import React from "react";
import useForm from "react-hook-form";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { StateMachineProvider, createStore } from "little-state-machine";
import { withRouter } from "react-router-dom";
import { useStateMachine } from "little-state-machine";
import updateAction from "./updateAction";
import { Button, Form, Input, Divider, Layout, Typography, Skeleton, Switch, Card, Icon, Avatar } from 'antd';
const { Content } = Layout
const { Text, Paragraph } = Typography;
const { Meta } = Card;
createStore({
data: {}
});
const General = props => {
const { register, handleSubmit, errors } = useForm();
const { action } = useStateMachine(updateAction);
const onSubit = data => {
action(data);
props.history.push("./ProposalMethod");
};
return (
<div>
<Content
style={{
background: '#fff',
padding: 24,
margin: "auto",
minHeight: 280,
width: '70%'
}}
>
<Form onSubmit={handleSubmit(onSubit)}>
<h2>Part 1: General</h2>
<Form.Item label="Title" >
<Input
name="title"
placeholder="Add a title"
ref={register({ required: true })}
/>
{errors.title && 'A title is required.'}
</Form.Item>
<Form.Item label="Subtitle" >
<Input
name="subtitle"
placeholder="Add a subtitle"
ref={register({ required: true })}
/>
{errors.subtitle && 'A subtitle is required.'}
</Form.Item>
<Form.Item>
<Button type="secondary" htmlType="submit">
Next
</Button>
</Form.Item>
</Form>
</Content>
</div>
);
};
export default withRouter(General);
react-hook-form author here. Antd Input component doesn't really expose inner ref, so you will have to register during useEffect, and update value during onChange, eg:
const { register, setValue } = useForm();
useEffect(() => {
register({ name: 'yourField' }, { required: true });
}, [])
<Input name="yourField" onChange={(e) => setValue('yourField', e.target.value)}
i have built a wrapper component to make antd component integration easier: https://github.com/react-hook-form/react-hook-form-input
import React from 'react';
import useForm from 'react-hook-form';
import { RHFInput } from 'react-hook-form-input';
import Select from 'react-select';
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
function App() {
const { handleSubmit, register, setValue, reset } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<RHFInput
as={<Select options={options} />}
rules={{ required: true }}
name="reactSelect"
register={register}
setValue={setValue}
/>
<button
type="button"
onClick={() => {
reset({
reactSelect: '',
});
}}
>
Reset Form
</button>
<button>submit</button>
</form>
);
}
This is my working approach:
const Example = () => {
const { control, handleSubmit, errors } = useForm()
const onSubmit = data => console.log(data)
console.log(errors)
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
rules={{ required: "Please enter your email address" }}
as={
<Form.Item
label="name"
validateStatus={errors.email && "error"}
help={errors.email && errors.email.message}
>
<Input />
</Form.Item>
}
/>
<Button htmlType="submit">Submit</Button>
</Form>
)
}
On writing such code:
<Input
name="subtitle"
placeholder="Add a subtitle"
ref={register({ required: true })}
/>
You assume that Input reference is bound to input, but that's not true.
In fact, you need to bind it to inputRef.input.
You can check it with the next code:
const App = () => {
const inputRef = useRef();
const inputRefHtml = useRef();
useEffect(() => {
console.log(inputRef.current);
console.log(inputRefHtml.current);
});
return (
<FlexBox>
<Input ref={inputRef} />
<input ref={inputRefHtml} />
</FlexBox>
);
};
# Logs
Input {props: Object, context: Object, refs: Object, updater: Object, saveClearableInput: function ()…}
<input></input>
Note that antd is a complete UI library (using 3rd party "helpers" should "turn a red light"), in particular, Form has a validator implemented, you can see a variety of examples in docs.
In Ant Design v4.x + react-hook-form v6.x. We can implement as normally
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '#hookform/resolvers/yup';
import { useIntl } from 'react-intl';
import { Input, Button, Form } from 'antd';
const SignInSchema = yup.object().shape({
email: yup.string().email().required(),
password: yup.string().required('required').min(6, 'passwordMin'),
});
interface PropTypes {
defaultValues?: {
email: string;
password: string;
};
handleFormSubmit: SubmitHandler<{ email: string; password: string }>;
}
function SignInForm({ defaultValues, handleFormSubmit }: PropTypes) {
const intl = useIntl();
const { handleSubmit, control, errors } = useForm({
defaultValues,
resolver: yupResolver(SignInSchema),
});
return (
<Form onFinish={handleSubmit(handleFormSubmit)}>
<Form.Item
validateStatus={errors && errors['email'] ? 'error' : ''}
help={errors.email?.message}
>
<Controller
as={Input}
name="email"
autoComplete="email"
control={control}
placeholder={intl.formatMessage({ id: 'AUTH_INPUT_EMAIL' })}
/>
</Form.Item>
<Form.Item
validateStatus={errors && errors['password'] ? 'error' : ''}
help={errors.password?.message}
>
<Controller
as={Input}
name="password"
type="password"
control={control}
autoComplete="new-password"
defaultValue=""
placeholder={intl.formatMessage({ id: 'AUTH_INPUT_PASSWORD' })}
/>
</Form.Item>
<Button type="primary" htmlType="submit">
{intl.formatMessage({ id: 'SIGN_IN_SUBMIT_BUTTON' })}
</Button>
</Form>
);
}
export default SignInForm;
In case anyone is still interested in getting close to the default styles of the Form Inputs provided by Ant, here's how I got it to work:
import { Form, Button, Input } from 'antd';
import { useForm, Controller } from 'react-hook-form';
function MyForm() {
const { control, handleSubmit, errors, setValue } = useForm();
const emailError = errors.email && 'Enter your email address';
const onSubmit = data => { console.log(data) };
const EmailInput = (
<Form.Item>
<Input
type="email"
placeholder="Email"
onChange={e => setValue('email', e.target.value, true)}
onBlur={e => setValue('email', e.target.value, true)}
/>
</Form.Item>
);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
as={EmailInput}
name="email"
control={control}
defaultValue=""
rules={{
required: true
}}
validateStatus={emailError ? 'error' : ''}
help={emailError}
/>
<Button block type="primary" htmlType="submit">
Submit
</Button>
</form>
);
}
Codesandbox sample

Resources