Use hooks to write a login form - Frontier Development Team
Recently I tried to write a login form using React hooks related API, the purpose is to deepen the understanding of hooks. This article will not explain the use of specific APIs, but will provide a step-by-step in-depth look at the functions to be implemented. So you need to have a basic understanding of hooks before reading. The final look is a bit like using hooks to write a simple redux-like state management model.
Fine-grained state
A simple login form contains three input items: username, password, and verification code, which also represent the three data states of the form. We simply focus on username , password, and capacha establish state relationships through useState
respectively, which is the so-called relatively fine-grained state division. The code is also very 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;
This kind of fine-grained state is very simple and intuitive, but if there are too many states, it will be quite troublesome to write the same logic for each state, and it will be too scattered. .
Coarse-grained
We define username, password, and capacha as a state, which is the so-called coarse-grained state division:
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> ); };
As you can see, the setXXX method is reduced, and the setState The naming is also more appropriate, but this setState will not automatically merge the status items, and we need to merge them manually.
Add form verification
Of course a complete form cannot lack the verification link. In order to display the error message below the input when an error occurs, we first extract a sub-component Field:
const Filed = ({ placeholder, value, onChange, error }) => { return ( <p> <input> {error && <span>error</span>} </p> ); };
We use the schema-typed library to do some field definition and verification. Its use is very simple. The API is similar to React's PropType. We define the following field verification:
const model = SchemaModel({ username: StringType().isRequired("用户名不能为空"), password: StringType().isRequired("密码不能为空"), captcha: StringType() .isRequired("验证码不能为空") .rangeLength(4, 4, "验证码为4位字符"), });
Then add errors to the state and trigger model.check
in the submit method. 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> ); };
Then when we click submit without entering anything, an error message will be triggered:
useReducer rewrite
At this point, it feels like our form is almost complete and the function seems to be completed. But is this okay? We print console.log(placeholder, "rendering")
in the Field component. When we enter the user name, we find that all the Field components are re-rendered. This can be optimized.
How to do it? First, let the Field component avoid re-rendering when the props remain unchanged. We use React.memo to wrap the Field component.
React.memo is a high-order component. It's very similar to React.PureComponent, but only works with functional components. If your function component renders the same result given the same props, then you can improve the performance of the component by remembering the component's rendering result by wrapping it in React.memo and calling it
export default React.memo(Filed);
But if this is the case, all the Field components will still be re-rendered. This is because our onChange function returns a new function object every time, causing the memo to become invalid.
We can wrap Filed's onChange function with useCallback
, so that we don't have to generate a new function object every time the component is rendered.
const changeUserName = useCallback((e) => { const value = e.target.value; setState((prevState) => { // 注意因为我们设置useCallback的依赖为空,所以这里要使用函数的形式来获取最新的state(preState) return { ...prevState, username: value, }; }); }, []);
Is there any other solution? We noticed useReducer,
useReducer is another alternative, which is more suitable for managing state objects containing multiple sub-values. It is an alternative to useState. It receives a reducer of the form (state, action) => newState and returns the current state and its matching dispatch method. Moreover, using useReducer can also optimize the performance of components that trigger deep updates, because you can pass dispatch to sub-components instead of callback functions.
An important feature of useReducer is that the dispatch it returns The function's identity is stable and does not change when the component is re-rendered. Then we can safely pass dispatch to the sub-component without worrying about causing the sub-component to re-render.
We first define the reducer function to operate state:
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; } }Correspondingly call userReducer in LoginForm, pass in our reducer function and initialState
const LoginForm = () => { const [state, dispatch] = useReducer(reducer, initialState); const submit = ... return ( <p> <field></field> ... <button>提交</button> </p> ); };in the Field subcomponent Add a new name attribute to identify the updated key, and pass in the dispatch method
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);. In this way, we can pass in dispatch to let the sub-component handle the change event internally and avoid passing in the onChange function. At the same time, the state management logic of the form is migrated to the reducer. Global storeWhen our component hierarchy is relatively deep and we want to use the dispatch method, we need to pass it through props layer by layer, which is obviously inconvenient. At this time we can use the Context API provided by React to share state and methods across components. Context provides a method to transfer data between component trees without manually adding props to each layer of components. Functional components can be implemented using createContext and useContext. We won’t talk about how to use these two APIs here. You can basically write them out by looking at the documentation. We use unstated-next to implement it, which is essentially an encapsulation of the above API and is more convenient to use.
我们首先新建一个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);
dispatch一个函数
使用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教程》
The above is the detailed content of Use hooks to write a login form - Frontier Development Team. For more information, please follow other related articles on the PHP Chinese website!

JavaScript is widely used in websites, mobile applications, desktop applications and server-side programming. 1) In website development, JavaScript operates DOM together with HTML and CSS to achieve dynamic effects and supports frameworks such as jQuery and React. 2) Through ReactNative and Ionic, JavaScript is used to develop cross-platform mobile applications. 3) The Electron framework enables JavaScript to build desktop applications. 4) Node.js allows JavaScript to run on the server side and supports high concurrent requests.

Python is more suitable for data science and automation, while JavaScript is more suitable for front-end and full-stack development. 1. Python performs well in data science and machine learning, using libraries such as NumPy and Pandas for data processing and modeling. 2. Python is concise and efficient in automation and scripting. 3. JavaScript is indispensable in front-end development and is used to build dynamic web pages and single-page applications. 4. JavaScript plays a role in back-end development through Node.js and supports full-stack development.

C and C play a vital role in the JavaScript engine, mainly used to implement interpreters and JIT compilers. 1) C is used to parse JavaScript source code and generate an abstract syntax tree. 2) C is responsible for generating and executing bytecode. 3) C implements the JIT compiler, optimizes and compiles hot-spot code at runtime, and significantly improves the execution efficiency of JavaScript.

JavaScript's application in the real world includes front-end and back-end development. 1) Display front-end applications by building a TODO list application, involving DOM operations and event processing. 2) Build RESTfulAPI through Node.js and Express to demonstrate back-end applications.

The main uses of JavaScript in web development include client interaction, form verification and asynchronous communication. 1) Dynamic content update and user interaction through DOM operations; 2) Client verification is carried out before the user submits data to improve the user experience; 3) Refreshless communication with the server is achieved through AJAX technology.

Understanding how JavaScript engine works internally is important to developers because it helps write more efficient code and understand performance bottlenecks and optimization strategies. 1) The engine's workflow includes three stages: parsing, compiling and execution; 2) During the execution process, the engine will perform dynamic optimization, such as inline cache and hidden classes; 3) Best practices include avoiding global variables, optimizing loops, using const and lets, and avoiding excessive use of closures.

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

Python and JavaScript have their own advantages and disadvantages in terms of community, libraries and resources. 1) The Python community is friendly and suitable for beginners, but the front-end development resources are not as rich as JavaScript. 2) Python is powerful in data science and machine learning libraries, while JavaScript is better in front-end development libraries and frameworks. 3) Both have rich learning resources, but Python is suitable for starting with official documents, while JavaScript is better with MDNWebDocs. The choice should be based on project needs and personal interests.


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

SublimeText3 English version
Recommended: Win version, supports code prompts!

mPDF
mPDF is a PHP library that can generate PDF files from UTF-8 encoded HTML. The original author, Ian Back, wrote mPDF to output PDF files "on the fly" from his website and handle different languages. It is slower than original scripts like HTML2FPDF and produces larger files when using Unicode fonts, but supports CSS styles etc. and has a lot of enhancements. Supports almost all languages, including RTL (Arabic and Hebrew) and CJK (Chinese, Japanese and Korean). Supports nested block-level elements (such as P, DIV),

SublimeText3 Mac version
God-level code editing software (SublimeText3)

MinGW - Minimalist GNU for Windows
This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

Atom editor mac version download
The most popular open source editor