ホームページ > 記事 > ウェブフロントエンド > フックを使用してログイン フォームを作成する - Frontier Development Team
最近、フックについての理解を深めることが目的で、React フック関連の API を使用してログインフォームを書いてみました。この記事では、特定の API の使用方法については説明しませんが、実装される機能を段階的に詳しく説明します。したがって、読む前にフックの基本を理解する必要があります。最終的な外観は、フックを使用して単純な Redux のような状態管理モデルを作成するのと少し似ています。
単純なログイン フォームには、ユーザー名、パスワード、確認コードという 3 つの入力項目が含まれており、これらはフォームの 3 つのデータ状態も表します。ここでは単に username に焦点を当てます。パスワードとカパチャは、それぞれ useState
を通じて状態関係を確立します。これは、いわゆる比較的粒度の細かい状態分割です。コードも非常にシンプルです:
// LoginForm.js const LoginForm = () => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [captcha, setCaptcha] = useState(""); const submit = useCallback(() => { loginService.login({ username, password, captcha, }); }, [username, password, captcha]); return ( <p> <input> { setUsername(e.target.value); }} /> <input> { setPassword(e.target.value); }} /> <input> { setCaptcha(e.target.value); }} /> <button>提交</button> </p> ); }; export default LoginForm;
この種のきめ細かい状態は非常にシンプルで直感的ですが、状態が多すぎると、各状態に対して同じロジックを記述するのは非常に面倒になります。散らかりすぎてしまいます。
ユーザー名、パスワード、およびカパチャを状態として定義します。これは、いわゆる粗粒度の状態分割です:
const LoginForm = () => { const [state, setState] = useState({ username: "", password: "", captcha: "", }); const submit = useCallback(() => { loginService.login(state); }, [state]); return ( <p> <input> { setState({ ...state, username: e.target.value, }); }} /> ... <button>提交</button> </p> ); };
ご覧のとおり、 、 setXXX メソッドが削減され、setState ネーミングもより適切になりますが、この setState はステータス項目を自動的にマージしないため、手動でマージする必要があります。
もちろん、完全なフォームに検証リンクが欠けていてはなりません。エラーが発生したときに入力の下にエラー メッセージを表示するために、最初にサブコンポーネント フィールドを抽出します。
const Filed = ({ placeholder, value, onChange, error }) => { return ( <p> <input> {error && <span>error</span>} </p> ); };
スキーマ型ライブラリを使用して、フィールドの定義と検証を行います。使い方はとても簡単です。API は React の PropType に似ています。次のフィールド検証を定義します:
const model = SchemaModel({ username: StringType().isRequired("用户名不能为空"), password: StringType().isRequired("密码不能为空"), captcha: StringType() .isRequired("验证码不能为空") .rangeLength(4, 4, "验证码为4位字符"), });
次に、状態にエラーを追加し、submit メソッドで model.check
をトリガーします。チェック。
const LoginForm = () => { const [state, setState] = useState({ username: "", password: "", captcha: "", // ++++ errors: { username: {}, password: {}, captcha: {}, }, }); const submit = useCallback(() => { const errors = model.check({ username: state.username, password: state.password, captcha: state.captcha, }); setState({ ...state, errors: errors, }); const hasErrors = Object.values(errors).filter((error) => error.hasError).length > 0; if (hasErrors) return; loginService.login(state); }, [state]); return ( <p> <field> { setState({ ...state, username: e.target.value, }); }} /> ... <button>提交</button> </field></p> ); };
次に、何も入力せずに送信をクリックすると、エラー メッセージが表示されます:
console.log(placeholder, "rendering") を出力します。ユーザー名を入力すると、すべての Field コンポーネントが再レンダリングされることがわかります。これは最適化できます。 ###どうやってするの?まず、プロパティが変更されていない場合に Field コンポーネントが再レンダリングされないようにするため、React.memo を使用して Field コンポーネントをラップします。
React.memo は上位コンポーネントです。これは React.PureComponent に非常に似ていますが、機能コンポーネントでのみ動作します。関数コンポーネントが同じ props を与えられて同じ結果をレンダリングする場合、コンポーネントのレンダリング結果を React.memo でラップしてそれを呼び出して記憶し、
ただし、この場合でも、すべてのフィールド コンポーネントが再レンダリングされます。これは、onChange 関数が毎回新しい関数オブジェクトを返すため、メモが無効になるためです。
useCallback
でラップできるため、コンポーネントがレンダリングされるたびに新しい関数オブジェクトを生成する必要がなくなります。 <pre class="brush:php;toolbar:false">const changeUserName = useCallback((e) => {
const value = e.target.value;
setState((prevState) => { // 注意因为我们设置useCallback的依赖为空,所以这里要使用函数的形式来获取最新的state(preState)
return {
...prevState,
username: value,
};
});
}, []);</pre>
他に解決策はありますか? useReducer、
useReducer が別の代替手段であり、複数のサブ値を含む状態オブジェクトの管理により適していることに気付きました。これは useState の代替です。 (state, action) => newState の形式のリデューサーを受け取り、現在の状態とそれに対応するディスパッチ メソッドを返します。さらに、useReducer を使用すると、コールバック関数の代わりにサブコンポーネントにディスパッチを渡すことができるため、詳細な更新をトリガーするコンポーネントのパフォーマンスを最適化することもできます。関数の ID は安定しており、コンポーネントが再レンダリングされても変わりません。そうすれば、サブコンポーネントが再レンダリングされることを心配することなく、サブコンポーネントにディスパッチを安全に渡すことができます。
まず、状態を操作するためのリデューサー関数を定義します。これに対応して、LoginForm で userReducer を呼び出し、リデューサー関数と initialStateconst initialState = { username: "", ... errors: ..., }; // dispatch({type: 'set', payload: {key: 'username', value: 123}}) function reducer(state, action) { switch (action.type) { case "set": return { ...state, [action.payload.key]: action.payload.value, }; default: return state; } }
const LoginForm = () => { const [state, dispatch] = useReducer(reducer, initialState); const submit = ... return ( <p> <field></field> ... <button>提交</button> </p> ); };
を Field サブコンポーネントに渡します。 新しい name 属性を追加します。更新されたキーを識別し、ディスパッチ メソッド
const Filed = ({ placeholder, value, dispatch, error, name }) => { console.log(name, "rendering"); return ( <p> <input> dispatch({ type: "set", payload: { key: name, value: e.target.value }, }) } /> {error && <span>{error}</span>} </p> ); }; export default React.memo(Filed);
グローバル ストア
コンポーネント階層が比較的深く、ディスパッチ メソッドを使用したい場合、レイヤーごとに props を介してメソッドを渡す必要がありますが、これは明らかに不便です。現時点では、React が提供する Context API を使用して、コンポーネント間で状態とメソッドを共有できます。
Context は、コンポーネントの各層に props を手動で追加することなく、コンポーネント ツリー間でデータを転送する方法を提供します。
これら 2 つの API の使用方法についてはここでは説明しませんが、基本的にはドキュメントを参照して記述することができます。これを実装するには unstated-next を使用します。これは本質的に上記の API のカプセル化であり、より使いやすくなっています。
我们首先新建一个store.js文件,放置我们的reducer函数,并新建一个useStore hook,返回我们关注的state和dispatch,然后调用createContainer并将返回值Store暴露给外部文件使用。
// store.js import { createContainer } from "unstated-next"; import { useReducer } from "react"; const initialState = { ... }; function reducer(state, action) { switch (action.type) { case "set": ... default: return state; } } function useStore() { const [state, dispatch] = useReducer(reducer, initialState); return { state, dispatch }; } export const Store = createContainer(useStore);
接着我们将LoginForm包裹一层Provider
// LoginForm.js import { Store } from "./store"; const LoginFormContainer = () => { return ( <store.provider> <loginform></loginform> </store.provider> ); };
这样在子组件中就可以通过useContainer随意的访问到state和dispatch了
// Field.js import React from "react"; import { Store } from "./store"; const Filed = ({ placeholder, name }) => { const { state, dispatch } = Store.useContainer(); return ( ... ); }; export default React.memo(Filed);
可以看到不用考虑组件层级就能轻易访问到state和dispatch。但是这样一来每次调用dispatch之后state都会变化,导致Context变化,那么子组件也会重新render了,即使我只更新username, 并且使用了memo包裹组件。
当组件上层最近的更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染
那么怎么避免这种情况呢,回想一下使用redux时,我们并不是直接在组件内部使用state,而是使用connect高阶函数来注入我们需要的state和dispatch。我们也可以为Field组件创建一个FieldContainer组件来注入state和dispatch。
// Field.js const Filed = ({ placeholder, error, name, dispatch, value }) => { // 我们的Filed组件,仍然是从props中获取需要的方法和state } const FiledInner = React.memo(Filed); // 保证props不变,组件就不重新渲染 const FiledContainer = (props) => { const { state, dispatch } = Store.useContainer(); const value = state[props.name]; const error = state.errors[props.name].errorMessage; return ( <filedinner></filedinner> ); }; export default FiledContainer;
这样一来在value值不变的情况下,Field组件就不会重新渲染了,当然这里我们也可以抽象出一个类似connect高阶组件来做这个事情:
// Field.js const connect = (mapStateProps) => { return (comp) => { const Inner = React.memo(comp); return (props) => { const { state, dispatch } = Store.useContainer(); return ( <inner></inner> ); }; }; }; export default connect((state, props) => { return { value: state[props.name], error: state.errors[props.name].errorMessage, }; })(Filed);
使用redux时,我习惯将一些逻辑写到函数中,如dispatch(login()),
也就是使dispatch支持异步action。这个功能也很容易实现,只需要装饰一下useReducer返回的dispatch方法即可。
// store.js function useStore() { const [state, _dispatch] = useReducer(reducer, initialState); const dispatch = useCallback( (action) => { if (typeof action === "function") { return action(state, _dispatch); } else { return _dispatch(action); } }, [state] ); return { state, dispatch }; }
如上我们在调用_dispatch方法之前,判断一下传来的action,如果action是函数的话,就调用之并将state、_dispatch作为参数传入,最终我们返回修饰后的dispatch方法。
不知道你有没有发现这里的dispatch函数是不稳定,因为它将state作为依赖,每次state变化,dispatch就会变化。这会导致以dispatch为props的组件,每次都会重新render。这不是我们想要的,但是如果不写入state依赖,那么useCallback内部就拿不到最新的state
。
那有没有不将state写入deps,依然能拿到最新state的方法呢,其实hook也提供了解决方案,那就是useRef
useRef返回的 ref 对象在组件的整个生命周期内保持不变,并且变更 ref的current 属性不会引发组件重新渲染
通过这个特性,我们可以声明一个ref对象,并且在useEffect
中将current
赋值为最新的state对象。那么在我们装饰的dispatch函数中就可以通过ref.current拿到最新的state。
// store.js function useStore() { const [state, _dispatch] = useReducer(reducer, initialState); const refs = useRef(state); useEffect(() => { refs.current = state; }); const dispatch = useCallback( (action) => { if (typeof action === "function") { return action(refs.current, _dispatch); //refs.current拿到最新的state } else { return _dispatch(action); } }, [_dispatch] // _dispatch本身是稳定的,所以我们的dispatch也能保持稳定 ); return { state, dispatch }; }
这样我们就可以定义一个login方法作为action,如下
// store.js export const login = () => { return (state, dispatch) => { const errors = model.check({ username: state.username, password: state.password, captcha: state.captcha, }); const hasErrors = Object.values(errors).filter((error) => error.hasError).length > 0; dispatch({ type: "set", payload: { key: "errors", value: errors } }); if (hasErrors) return; loginService.login(state); }; };
在LoginForm中,我们提交表单时就可以直接调用dispatch(login())
了。
const LoginForm = () => { const { state, dispatch } = Store.useContainer(); ..... return ( <p> <field></field> .... <button> dispatch(login())}>提交</button> </p> ); }
一个支持异步action的dispatch就完成了。
推荐教程:《JS教程》
以上がフックを使用してログイン フォームを作成する - Frontier Development Teamの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。