Maison > Article > interface Web > Utilisez des hooks pour rédiger un formulaire de connexion - Frontier Development Team
Récemment, j'ai essayé d'écrire un formulaire de connexion en utilisant l'API liée aux hooks React, le but est d'approfondir ma compréhension des hooks. Cet article n’expliquera pas l’utilisation d’API spécifiques, mais fournira un aperçu approfondi, étape par étape, des fonctions à implémenter. Vous devez donc avoir une compréhension de base des hooks avant de lire. Le look final ressemble un peu à l'utilisation de hooks pour écrire un simple modèle de gestion d'état de type redux.
Un formulaire de connexion simple contient trois éléments d'entrée : le nom d'utilisateur, le mot de passe et le code de vérification, qui représentent également les trois états de données du formulaire. Nous nous concentrons simplement sur le nom d'utilisateur, mot de passe et capacha établissent respectivement des relations de statut via useState
, qui est ce qu'on appelle la division de statut relativement fine. Le code est également très simple :
// 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;
Ce type d'état à granularité fine est très simple et intuitif, mais s'il y a trop d'états, il sera assez gênant d'écrire la même logique pour chaque état, et ce sera trop dispersé.
Nous définissons le nom d'utilisateur, le mot de passe et la capacha comme un État, ce qu'on appelle la division d'État à gros grain :
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> ); };
Comme vous pouvez le voir , la méthode setXXX est réduite et la dénomination setState est également plus appropriée, mais ce setState ne fusionnera pas automatiquement les éléments d'état et nous devons les fusionner manuellement.
Bien sûr, un formulaire complet ne peut pas manquer du lien de vérification. Afin d'afficher le message d'erreur sous la saisie lorsqu'une erreur se produit, nous extrayons d'abord un champ de sous-composant :
const Filed = ({ placeholder, value, onChange, error }) => { return ( <p> <input> {error && <span>error</span>} </p> ); };
Nous utilisons la bibliothèque typée par schéma pour effectuer des définitions et des vérifications de champs. C'est très simple à utiliser. L'API est similaire au PropType de React. Nous définissons la vérification du champ suivant :
const model = SchemaModel({ username: StringType().isRequired("用户名不能为空"), password: StringType().isRequired("密码不能为空"), captcha: StringType() .isRequired("验证码不能为空") .rangeLength(4, 4, "验证码为4位字符"), });
puis ajoutons des erreurs à l'état et déclenchons model.check
dans la méthode de soumission pour vérification.
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> ); };
Ensuite, lorsque nous cliquons sur soumettre sans rien saisir, un message d'erreur sera déclenché :
À à ce stade, on a l'impression que notre formulaire est presque prêt et que la fonction semble terminée. Mais est-ce que ça va ? Nous imprimons console.log(placeholder, "rendering")
dans le composant Field Lorsque nous entrons le nom d'utilisateur, nous constatons que tous les composants Field sont restitués. Cela peut être optimisé.
Comment faire ? Premièrement, nous voulons que le composant Field évite le nouveau rendu lorsque les accessoires restent inchangés. Nous utilisons React.memo pour envelopper le composant Field.
React.memo est un composant d'ordre élevé. C'est très similaire à React.PureComponent, mais ne fonctionne qu'avec des composants fonctionnels. Si votre composant de fonction rend le même résultat avec les mêmes accessoires, vous pouvez alors améliorer les performances du composant en mémorisant le résultat du rendu du composant en l'enveloppant dans React.memo et en l'appelant
export default React.memo(Filed);
Mais juste dans ce cas, les composants Field sont tous restitués. En effet, notre fonction onChange renvoie un nouvel objet fonction à chaque fois, ce qui rend le mémo invalide.
Nous pouvons envelopper la fonction onChange de Filed avec useCallback
afin de ne pas avoir à générer un nouvel objet fonction à chaque fois que le composant est rendu.
const changeUserName = useCallback((e) => { const value = e.target.value; setState((prevState) => { // 注意因为我们设置useCallback的依赖为空,所以这里要使用函数的形式来获取最新的state(preState) return { ...prevState, username: value, }; }); }, []);
Existe-t-il une autre solution ? Nous avons remarqué que useReducer,
useReducer est une autre alternative, plus adaptée à la gestion d'objets d'état contenant plusieurs sous-valeurs. C'est une alternative à useState. Il reçoit un réducteur de la forme (state, action) => newState et renvoie l'état actuel et sa méthode de répartition correspondante. De plus, l'utilisation de useReducer peut également effectuer une optimisation des performances pour les composants qui déclenchent des mises à jour approfondies, car vous pouvez transmettre la répartition aux sous-composants au lieu des fonctions de rappel
Une caractéristique importante de useReducer est qu'il renvoie dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变
. Nous pouvons ensuite transmettre en toute sécurité la répartition au sous-composant sans nous soucier du nouveau rendu du sous-composant.
Nous définissons d'abord la fonction de réduction pour faire fonctionner l'état :
const 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; } }
appelons en conséquence userReducer dans LoginForm, transmettons notre fonction de réduction et initialState
const LoginForm = () => { const [state, dispatch] = useReducer(reducer, initialState); const submit = ... return ( <p> <field></field> ... <button>提交</button> </p> ); };
dans le sous-composant Field Ajoutez un nouvel attribut de nom pour identifier la clé mise à jour et transmettre la méthode de répartition
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);
De cette façon, nous pouvons transmettre la méthode de répartition pour laisser le sous-composant gérer l'événement de changement en interne et éviter de transmettre la fonction onChange. Dans le même temps, la logique de gestion d’état du formulaire est migrée vers le réducteur.
Lorsque notre hiérarchie de composants est relativement profonde et que nous souhaitons utiliser la méthode de répartition, nous devons la transmettre couche par couche à travers les accessoires, ce qui est évidemment gênant. À l'heure actuelle, nous pouvons utiliser l'API Context fournie par React pour partager l'état et les méthodes entre les composants.
Context fournit une méthode pour transférer des données entre les arborescences de composants sans ajouter manuellement d'accessoires pour chaque couche de composants
Les composants fonctionnels peuvent être implémentés à l'aide de createContext et useContext.
Ici, nous ne parlerons pas de la façon d'utiliser ces deux API. Vous pouvez essentiellement les écrire en consultant la documentation. Nous utilisons unstated-next pour l'implémenter, qui est essentiellement une encapsulation de l'API ci-dessus et est plus pratique à utiliser.
我们首先新建一个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教程》
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!