跳至主要内容

[note] Formik 筆記

關於那些小細節 Tips

  • Formik 會使用 <input> 欄位中的 name 屬性來決定該 value 要保存在哪個內部的 key 中。
  • Formik 中包括 values, errors, touched 物件,裡面的 key 都會和 <input>name 對應。
  • Formik 在使用 radio button 時,當帶入的 value 是「數值」需特別留意,因為存在 field 的值會是「數值」,但存入 Formik 的 meta 中會變成「字串」,這會導致 Formik 沒辦法匹配正確,進而使得該 input 沒辦法被勾選(checked),但在 Formik 中又會設值的情況(參考這個 Gist)。

useFormik

基本使用

使用 Formik 中的 values 物件:

import { useFormik } from 'formik';

const SignupForm = () => {
// STEP 1: useFormik
const formik = useFormik({
// STEP 2-1:建立初始值
initialValues: { email: '' },
// STEP 2-2:在 onSubmit 時透過 `values` 物件取得表單中的內容
onSubmit: values => {
console.log(JSON.stringify(values, null, 2));
},
});

return (
// STEP 3: 在 onSubmit 事件中帶入 `formik.handleSubmit`
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>
{/* STEP 4-1:建立 input 欄位,formik 會用 input 的 name 來作為內部的 key */}
<input id="email" name="email" type="email"
{/* STEP 4-2: invoke `formik.handleChange` when input onchange */}
onChange={formik.handleChange}

{/* STEP 4-3: 使用 values 物件取回該欄位的值 */}
value={formik.values.email}
/>
</div>
<button type="submit">Submit</button>
</form>
);
};

表單驗證:手動處理(validate)

使用 Formik 中的 errors 物件,並搭配 validate 屬性:

// STEP 1:建立 validate
const validate = (values) => {
const errors = {};

if (!values.firstName) {
errors.firstName = 'Required first name';
} else if (values.firstName.length > 15) {
errors.firstName = 'Must be 15 characters or less';
}

return errors;
};

const SignupForm = () => {
const formik = useFormik({
initialValues: { email: '' },
// STEP 2:放入 validate 函式
validate,
onSubmit: (values) => {
console.log(JSON.stringify(values, null, 2));
},
});

return (
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>
<input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
value={formik.values.email}
/>
{/* STEP 3: 有錯誤時顯示錯誤 */}
{formik.errors.email ? <div>{formik.errors.email}</div> : null}
</div>

<button type="submit">Submit</button>
</form>
);
};
  • 預設的情況下,在每一次 onChange 及 onSubmit 時,Formik 都會促發 validate 函式(如果有給的話),只有在 errors 為空物件時,才會真的把表單送出。

表單驗證:搭配 Yup(validationSchema)

在 Formik 中除了 validate 可以手動驗證表的欄外之外,額外提供 validationSchema 可以搭配 Yup 來做欄位的資料驗證:

import React from 'react';
import { useFormik } from 'formik';

// STEP 1:匯入 Yup
import * as Yup from 'yup';

const SignupForm = () => {
const formik = useFormik({
initialValues: { email: '' },

// STEP 2:搭配 Yup 使用 validationSchema
validationSchema: Yup.object({
firstName: Yup.string().max(15, '至少要超過 15 個字').required('firstName 為必填'),
email: Yup.string().email('無效的 Email').required('email 為必填'),
}),
onSubmit: (values) => {
console.log(JSON.stringify(values, null, 2));
},
});

return (
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>
<input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? <div>{formik.errors.email}</div> : null}
</div>

<button type="submit">Submit</button>
</form>
);
};

檢驗表單是否碰過

使用 Formik 中的 touched 物件,搭配在 input 欄位上的 onBlur 事件:

const SignupForm = () => {
const formik = useFormik({
/* ... */
});

return (
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>
<input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
value={formik.values.email}
// STEP 1: 使用 onBlur 搭配 formik.handleBlur
onBlur={formik.handleBlur}
/>

{/* STEP 2: 取得 touched 物件的值 */}
{formik.touched.firstName && formik.errors.firstName ? (
<div>{formik.errors.firstName}</div>
) : null}
</div>
<button type="submit">Submit</button>
</form>
);
};

簡化程式碼 - getFieldProps()

在上面的範例中可看到需要自己在 <input> 中寫 onChange, onBlur, value 等等,formik 提供一個名為 getFiledProps 的 helper 可以簡化這些程式碼:

import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';

const SignupForm = () => {
const formik = useFormik({
/* ... */
});

return (
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email Address</label>

{/**
* STEP 1:使用 `formik.getFieldProps()` 可以省去要自己註冊事件的時間
**/}
<input name="email" {...formik.getFieldProps('email')} />
{formik.touched.email && formik.errors.email ? <div>{formik.errors.email}</div> : null}
</div>

<button type="submit">Submit</button>
</form>
);
};

使用 Formik 元件,進一步簡化程式碼

在 Formik 中進一步提供 <Formik> 這個元件來簡化表單的程式架構,它利用了 React Context API 來實作,使用 <Formik /> 元件後,內部可以在使用 <Form />, <Field /><ErrorMessage />

先將 useFormik 改成用 Formik

Imgur

進一步搭配 <Form>, <Field>, ErrorMessage,程式碼變得更簡潔:

Imgur

API

Formik

<Formik /> @ Formik API

<Formik /> 中上可用屬性和對應的 helper 可參考 API 文件

/* demo use for array of objects with formik */
import React from 'react';
import { Formik, Field, Form } from 'formik';

const Invitation = () => (
<div>
<Formik
initialValues={initialValues}
onSubmit={(values) => {
/* values to submit ... */
}}
validationSchema={
{
/* validation rules here */
}
}
>
{(formik) => <FormikFormComponent />}
</Formik>
</div>
);

export default Invitation;

Form and Field

<Field /> @ Formik API Docs

<Form className="col s-12">
{/* 添加 label */}
<label htmlFor="firstName">First Name</label>
<Field name="firstName" placeholder="Jane" />

{/* children 可以是 function:如果需要自己控制 input */}
<Field name="user.name" type="text">
{({ field, form, meta }) => <input {...field} type="text" placeholder="Jane Doe" />}
</Field>

{/* children 也可以是 JSX */}
<Field name="color" as="select" placeholder="Favorite Color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</Field>

<Field name="user.email" type="email" placeholder="jane@example.com" />
</Form>

FieldArray

keywords: push, remove

從原本的 <Form> 改成帶有 <FieldArray> 的 form。

如果資料是 array of objects 可以使用 <FieldArray>,它會多了 pushremove 的方法,方便我們進行資料的新增和刪除:

<Form>
<FieldArray name="friends">{({ push, remove }) => <FormikFormComponent />}</FieldArray>
</Form>

假設原本的資料是這樣,需要使用 friends[0] 才可以取到值:

const values = {
friends: [{ name: '', email: '' }],
};

const Form = () => (
<Form>
{/* ... */}
<Field name="friends[0].email" type="email" placeholder="jane@example.com" />
{/* ... */}
</Form>
);

如果資料是 Array of Objects 的話,可以使用 map 搭配 pushremove 即可達到新增刪除陣列元素的效果:

<Formik initialValues={initialValues} onSubmit={(values) => {}}>
{({ values, isSubmitting }) => (
<React.Fragment>
<Form className="col s-12">
<FieldArray name="friends">
{({ push, remove }) => (
<React.Fragment>
{values.friends &&
values.friends.length > 0 &&
values.friends.map((value, index) => (
<React.Fragment>
<Field name={`friends[${index}].name`} type="text" placeholder="Jane Doe" />

<Field
name={`friends[${index}].email`}
type="email"
placeholder="jane@example.com"
/>

{/* 使用 remove 移除陣列內的元素 */}
<button type="button" onClick={() => remove(index)}>
X
</button>
</React.Fragment>
))}
{/* 使用 push 添加陣列內的元ㄓㄨ */}
<button type="button" onClick={() => push({ name: '', email: '' })}>
Add Friend
</button>
</React.Fragment>
)}
</FieldArray>
</Form>
</React.Fragment>
)}
</Formik>

ErrorMessage

  • STEP 1: 在 <Formik /> 定義 validationSchema 屬性
  • STEP 2: 使用 ErrorMessage Component
import React from 'react';
import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';

// STEP 1: 在 <Formik /> 定義 validationSchema 屬性
<Formik
initialValues={initialValues}
validationSchema={Yup.object({
friends: Yup.array().of(
Yup.object({
name: Yup.string().required('Required'),
email: Yup.string().email('Invalid Email').required('Required'),
}),
),
})}
>
{({ values, isSubmitting }) => (
<Form className="col s-12">
<Field name={`friends[${index}].name`} type="email" placeholder="Jane Doe" />
{/* STEP 2: 使用 ErrorMessage Component */}
<ErrorMessage name={`friends[${index}].name`}>{(msg) => <span>{msg}</span>}</ErrorMessage>
</Form>
)}
</Formik>;

Debug

先到官網下載 Debug Component 的原始碼,接著可以把它 import 近來使用:

// 想要 debug formik 的地方
import React from 'react';
import { Formik, Field, Form } from 'formik';
import { Debug } from './Debug';

// ...
<Formik
initialValues={initialValues}
onSubmit={(values) => {}}
render={() => (
<Form>
<label htmlFor="firstName">First Name</label>
<Field name="firstName" placeholder="Jane" />

<button type="submit">Submit</button>

{/* ✨ put DEBUG COMPONENT in the form */}
<Debug />
</Form>
)}
/>;

useField

import React from 'react';
import { Formik, Form, useField } from 'formik';

// 定義自己的 input 欄位
function MyTextField(label, ...props) {
// 在 useField 的參數中可以帶入原本會放入 <Field /> 中的參數,例如 name, validate
// 會回傳 field, meta 和 helpers
const [field, meta, helpers] = useField(props.name);

return (
<React.Fragment>
<label htmlFor={props.id || props.name}>{label}</label>
<input {...field} {...props} />
{meta.error && meta.touched && <div>{meta.error}</div>}
</React.Fragment>
);
}

const Example = () => (
<Formik
initialValues={{
email: '',
firstName: 'red',
lastName: '',
}}
onSubmit={(values, actions) => {
console.log(JSON.stringify(values, null, 2));
}}
>
{(props) => (
<Form>
{/* 使用自定義的欄位 */}
<MyTextField name="firstName" type="text" label="First Name" />
<MyTextField name="lastName" type="text" label="Last Name" />
<MyTextField name="email" type="email" label="Email" />
<button type="submit">Submit</button>
</Form>
)}
</Formik>
);

field, meta, helpers 分別會得到:

field: {
name: 'firstName',
value: '',
}

meta: {
value: '',
error: 'Required',
touched: true,
initialValue: '',
initialTouched: false,
initialError: undefined,
}

helpers: {
setValue,
setTouched,
setError,
}

Customization

setFieldValue

setFieldValue

setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void

Custom onChange with callback to Formik parent @ Github Issue

class PersonalInfo extends React.Component {
constructor(props) {
this.handleCountryChange = this.handleCountryChange.bind(this);
}

// STEP 3: 在 onchange 的時候透過 setFieldValue 同步 formik 內的狀態
handleCountryChange(setFieldValue) {
return (e) => {
const countryId = e.target.value;
setFieldValue('country', countryId);

// 做些想要客製化的事情...
// this.fetchCityOptions({ countryId });
};
}

render() {
return (
<Formik
initialValues={{
country: '',
}}
onSubmit={(values) => {}}
// STEP 1: 取得 setFieldValue 方法可以用來更改 formik 內的狀態
render={({ setFieldValue }) => (
<Form>
<Field
css={inputSelect}
style={{ marginRight: 20 }}
component="select"
name="country"
placeholder="Country"
// STEP 2: 把 setFieldValue 的方法傳進去
onChange={this.handleCountryChange(setFieldValue)}
>
{countryOptions.map(({ id, name }) => (
<option key={id} value={id}>
{name}
</option>
))}
</Field>
</Form>
)}
/>
);
}
}

參考資源