redux-formの使い方とデータフローの解説
背景
実務でredux-formを使用することが多いが、実際どのような仕組みでデータフローが行われているのか理解があいまいだったので、redux-form ドキュメントのGetting Started With redux-formを読みつつ自分なりに重要だと思った部分をまとめてみました。
redux-formの使用を検討している方の参考になれば幸いです。
redux-formとは?
入力フォームなどに入力された値をstore(stateを管理している場所)へdispatchしてくれるライブラリのことです。
Reduxフォームに対応したフォーム状態の管理 - 開発者ドキュメント
データフローについての解説
(下の説明の番号とリンクさせ、どんなことをおこなっているのか分かりやすくしました。)
まず、注目すべき点は、reduxForm() でラップされたフォームコンポーネントです。(下半分の点線で囲まれた部分)
Inputにはテキスト入力がひとつあり、Field componentでラップされています。
(要するにField componentのこと。水色で囲まれた部分)
データは以下のように流れます。
①ユーザーがInput(redux-formのField component部分)をクリックする。
②「フォーカスアクション(要素がフォーカスされた時に発生するActionのこと。今回の要素はField componentでラップされたInputのこと)」 がdispatchされます。
③ formReducer が対応するstate slice(reducerとactionがセットになったもの)を更新します。
④その後stateがInput(redux-formのField component部分)に戻されます。
テキストを入力することやstateの変更、フォームの送信など、他の相互作用も同様に上記のことが起こります。
redux-formには他にも、validationやフォーマットハンドラ用のフック、様々なプロパティ、アクションクリエータなどがあります。
基本的な使い方(ドキュメントを日本語訳し、redux-formの使い方をまとめました。)
Step 1 formReducerをreducerとしてstoreに渡す
まず、Storeはフォームコンポーネントから来るActionを処理する方法を知っている必要があるため、formReducer をStoreに渡す必要があります。
(上記のデータフローの図だと③あたりのstoreの箇所にformReducerを作成することを指していると思います。)
これはすべてのフォームコンポーネントに対して機能するので、一度だけ渡す必要があります。
また、redux-form reducer に渡すキーは「 form」 という名前にする必要があります。
もし何らかの理由で独自のキーが必要な場合は、 getFormState config を参照してください。
import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
const rootReducer = combineReducers({
form: formReducer
})
const store = createStore(rootReducer)
Step 2 componentをreduxFormでラッピングする。
次に、フォームコンポーネントをStoreと通信させるためにreduxForm() でラップする必要があります。
このようにすることで、フォームのstateに関するprposと送信処理を処理する関数(おそらくhandleSubmitのことかも)を提供することができるようになります。
redux-formを使用するためのラッピング方法は、redux-formを使用したいcomponent名と一番下の()の中のcomponent名を一致させます。
そして、form:の部分に適当なフォーム名をつけます。
// redux-form
import { Field, reduxForm } from 'redux-form';
const CardoInfo = (() => {
return (
<>
--- 省略 ---
</>
)
})
export default reduxForm({
form: 'CreditValidation'
})(CardoInfo)
redux dev toolを使用すると以下のように表記されます。注目してほしい部分は、赤丸の箇所です。
ここで作成したformの名前がredux dev toolだと、stateボタンのform配下に表示されます。
redux-formを使用した時は、redux dev toolのStateタブの「form : 〇〇」で指定した〇〇を確認することでstateの中身を確認することができます。
Step 3 Field componentを作成
<Field name="inputName" component="input" type="text" />
次にredux-formのcomponentを作成します。今回は、Field componentの使用方法を紹介していきます。
まず、redux-formのField componentを用意し、そこにpropsとして必須の「name」と「component」を設定することで、Field componentからActionに基づいてStoreにデータが保存される入力フォームとして使用できるようになります。
(Field componentは、HTMLの<input/>要素(textタイプ)を作成します。)
また、Field componentはドキュメントにも詳細がありますが、value、onChange、onBlurなどの追加のpropsを渡すことができます。
さらに基本的な入力タイプ(上記で言うとcomponent="input"とかき、通常の入力フォームとして使用すること)とは別にクラスやステートレス関数を取り込むことができます。
送信されたデータは、JSONオブジェクトとしてonSubmit関数に渡されます。
実際に今回使用した Field component の使用例を見てみます。
Fieldコンポーネントはredux-formの中でよく使用されるcomponentです。
使用するにはnameとcomponentをpropsとして付け加えることが必須です。
使用例
<Field name="myField" component={renderField}/>
Fieldには様々なpropsが提供されており、nameやcomponentのほかに、typeで入力フォームの形式(ラジオボタンにするためradio、チェックボックスにするためにcheckboxなど)を変換したり、onChangeでコールバック関数を設定し入力フォームに値が入力されたときにコールバック関数を実行することができるようになります。
また、Field component に propsとして設定したステートレス関数には、「inputオブジェクト」と「metaオブジェクト」がpropsとして渡されます。
今回は下記のようなアプリを作成してみました。
src/App.jsx
// components
import UserForm from './components/UserForm';
// function
import showResults from "./components/func/showResults";
function App() {
return (
<div className="App">
<UserForm onSubmit={showResults} />
</div>
);
}
export default App;
components/func/showResults.js(onSubmitしたときにアラートメッセージを表示させる関数)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
export default (async function showResults(values) {
// 入力値があるかどうか判定
function isEmpty(obj) {
return !Object.keys(obj).length
}
if (isEmpty(values)) {
alert('値がありません。何か入力してください。')
} else {
await sleep(500);
window.alert(`あなたは以下のデータを送信しました。:\n\n${JSON.stringify(values, null, 2)}`);
}
});
src/components/UserForm.jsx(入力フォームを集めたcomponent)
import React from 'react'
import { useSelector, useDispatch } from 'react-redux';
import { reduxForm ,Field } from 'redux-form';
import renderField from './renderField';
import { createUserInfo, updateJob, updateAge, updateBloodType, updateUseBicycle, clearUserInfo, updateFavoriteColor, updateBirthday, updateEmail, updatePhysicalStrength } from '../modules/user';
// MUI
import { Button } from '@material-ui/core';
const UserForm = ({ handleSubmit, pristine, submitting, reset }) => {
const user = useSelector(state => state.user)
const { name, job, age, bloodType, useBicycle, favoriteColor, birthday, email, physicalStrength } = user
const dispatch = useDispatch()
// クリアボタンを押したときに入力フォームの値と表示されているテキストを元に戻す
const clear = () => {
reset()
dispatch(clearUserInfo())
}
return (
<>
<h4>自己紹介</h4>
<div>名前:{name}</div>
<div>職業:{job}</div>
<div>年齢:{age === "40" ? `${age}歳以上` : `${age}歳`}</div>
<div> 血液型:{`${bloodType}型`}</div>
<div>自転車を持っているかどうか?:{ useBicycle ? "持っている。" : "持っていない。"}</div>
<div>好きな色:{favoriteColor}</div>
<div>生年月日:{birthday}</div>
<div>Eメールアドレス:{email}</div>
<div>現在の体力:{physicalStrength}</div>
<br></br>
<div className='container'>
<form onSubmit={handleSubmit}>
<br></br>
<div>
名前<Field name="username" component={renderField} type="textarea" onChange={(e) => dispatch(createUserInfo(e))} />
</div>
<br></br>
<div>
職業<Field name="job" component={renderField} type="textarea" onChange={(e) => dispatch(updateJob(e))} />
</div>
<br></br>
<div>
年齢:
<label>
<Field name="age" component="input" type="radio" value="10~19" onChange={(e) => dispatch(updateAge(e))} />10〜19歳
</label>
<label>
<Field name="age" component="input" type="radio" value="20~39" onChange={(e) => dispatch(updateAge(e))} />20〜39歳
</label>
<label>
<Field name="age" component="input" type="radio" value="40" onChange={(e) => dispatch(updateAge(e))} />40歳以上
</label>
</div>
<br></br>
<div>
<label>血液型:
<Field name="bloodType" component="select" onChange={(e) => dispatch(updateBloodType(e))}>
<option value="A">A</option>
<option value="B">B</option>
<option value="O">O</option>
<option value="AB">AB</option>
</Field>
</label>
</div>
<br></br>
<div>
自転車を持っていますか?(持っている方はチェックしてください。)
<Field name="useBicycle" component={renderField} type="checkbox" onChange={(e) => dispatch(updateUseBicycle(e))} />
</div>
<br></br>
<br></br>
<div>
好きな色<Field name="favoritecolor" component={renderField} type="color" onChange={(e) => dispatch(updateFavoriteColor(e))} />
</div>
<br></br>
<br></br>
<div>
生年月日<Field name="birthday" component={renderField} type="date" onChange={(e) => dispatch(updateBirthday(e))} />
</div>
<br></br>
<br></br>
<div>
メールアドレス<Field name="email" component={renderField} type="email" onChange={(e) => dispatch(updateEmail(e))} />
</div>
<br></br>
<br></br>
<div>
現在の体力ゲージ<Field name="physicalStrength" component={renderField} type="range" onChange={(e) => dispatch(updatePhysicalStrength(e))} />
</div>
<br></br>
<Button color="secondary" variant="contained" type="submit">送信</Button>
<Button color="secondary" variant="contained" disabled={pristine || submitting} onClick={clear}>クリア</Button>
</form>
</div>
</>
)
}
export default reduxForm({
form: 'mainForm',
})(UserForm)
src/components/renderField.jsx(入力値により返り値が変化するステートレス関数)
import React from "react"
const renderField = (({input, label, type, meta: { touched, error, warning }}) => {
return (
<>
<label>{label}</label>
<div className='alert-message'>
<input {...input} placeholder={label} type={type}/>
{touched && ((error && <span>{error}</span>) || (warning && <span>{warning}</span>))}
</div>
</>
)
});
export default renderField;
src/modules/user/index.js(initialState,Reducer,Action Creatorをまとめたファイル)
// initialState
const initialState = {
name: "さとる",
job: "スーパーサラリーマン/デビルハンター",
age: 30,
bloodType: "A",
useBicycle: false,
favoriteColor: "ff0000",
birthday: "1993-02-21",
email: "satoru@gmail.com",
physicalStrength: "50"
}
// reducer
export const user = (state = initialState, action) => {
switch(action.type) {
case 'CLEAR_USER_INFO':
return initialState
case 'UPDATE_USER_INFO':
return { ...state, name: action.payload }
case 'UPDATE_JOB':
return { ...state, job: action.payload }
case 'UPDATE_AGE':
return { ...state, age: action.payload }
case 'UPDATE_BLOODTYPE':
return { ...state, bloodType: action.payload }
case 'UPDATE_USEBICYCLE':
return { ...state, useBicycle: action.payload }
case 'UPDATE_FAVORITECOLOR':
return { ...state, favoriteColor: action.payload }
case 'UPDATE_BIRTHDAY':
return { ...state, birthday: action.payload }
case 'UPDATE_EMAIL':
return { ...state, email: action.payload }
case 'UPDATE_PHYSICALSTRENGTH':
return { ...state, physicalStrength: action.payload }
default:
return state
}
}
// Action Creator
export const clearUserInfo = () => {
return {
type: 'CLEAR_USER_INFO'
}
}
export const createUserInfo = (e) => {
return {
type: 'UPDATE_USER_INFO',
payload: e.target.value
}
}
export const updateJob = (e) => {
return {
type: 'UPDATE_JOB',
payload: e.target.value
}
}
export const updateAge = (e) => {
return {
type: 'UPDATE_AGE',
payload: e.target.value
}
}
export const updateBloodType = (e) => {
return {
type: 'UPDATE_BLOODTYPE',
payload: e.target.value
}
}
export const updateUseBicycle = (e) => {
return {
type: 'UPDATE_USEBICYCLE',
payload: e.target.checked
}
}
export const updateFavoriteColor = (e) => {
return {
type: 'UPDATE_FAVORITECOLOR',
payload: e.target.value
}
}
export const updateBirthday = (e) => {
return {
type: 'UPDATE_BIRTHDAY',
payload: e.target.value
}
}
export const updateEmail = (e) => {
return {
type: 'UPDATE_EMAIL',
payload: e.target.value
}
}
export const updatePhysicalStrength = (e) => {
return {
type: 'UPDATE_PHYSICALSTRENGTH',
payload: e.target.value
}
}
以上が、redux-formの使い方の説明となります。
今回はField componentを例にredux-formの使用方法を紹介してみました。
まとめ
今回はredux-formの使用方法とデータフローについて解説しました。
最初はredux-formのドキュメントは自分としては少し理解するのに難しいと感じていましたが、何回も熟読し、自分なりにまとめてみるとある程度理解することができました。
ドキュメントを見ると自分がよく使用するField component以外にもFieldsやFormSectionといったものもあるらしいので、今後はこれらのcomponentを使用例などを交えて紹介することができたらいいなと思いました。
Discussion