[note] Formik 筆記
- Formik @ Official Website
- Formik Examples @ Github
- Formik Debug Component @ Github
關於那些小細節 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
:
進一步搭配 <Form>
, <Field>
, ErrorMessage
,程式碼變得更簡潔:
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>
,它會多了 push
和 remove
的方法,方便我們進行資料的新增和刪除:
<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
搭配 push
和 remove
即可達到新增刪除陣列元素的效果:
<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: (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>
)}
/>
);
}
}
參考資源
- Formik 的原始碼基礎可以參考 gist。
- New Examples / Sandboxes @ Github Issue
- Formik Examples @ Andreyco Github
- Radio & checkbox inputs with Formik @ Code Sandbox