经过前面几篇文章的介绍,我们学习了许多常使用的 hooks,不过除了那些 hooks 之外,我们也可以将一些常用的逻辑抽取出来封装到一个函数里,这就是 Custom Hook。
在了解什么是 Custom Hook 后,我们还是通过一个计时器案例快速熟悉下 Custom Hook 。这个案例比较简单,界面包含两个计时器,1个累加的计时器,1个相减的计时器,如下图所示。
在使用 Custom hook 之前,我们先使用常规的 hooks 编写本案例,首先我们新建 card 组件,用做显示计时器的容器,示例代码如下:
import classes from "./Card.module.css"; const Card = (props) =>{ return <div className={classes.card}>{props.children}</div> } export default Card;
接下来使用 Hook 创建 ForwardCounter
累加计时器,示例代码如下:
import {useState,useEffect} from "react"; import Card from './Card' const ForwardCounter= () => { const [counter,setCounter] = useState(0); useEffect(() => { const interval =setInterval(()=>{ setCounter((prevCounter) => prevCounter + 1); },1000); return () => clearInterval(interval) }, []); return <Card>{counter}</Card> } export default ForwardCounter;
接下来使用 Hook 创建 BackwardCounter
递减的计时器,示例代码如下:
import {useState,useEffect} from "react"; import Card from "./Card"; const BackwardCounter = ()=>{ const [counter,setCounter]=useState(0); useEffect(()=>{ const interval = setInterval(()=>{ setCounter((prevCounter)=>prevCounter-1) },1000); return ()=>clearInterval(interval); },[]) return <Card>{counter}</Card> } export default BackwardCounter;
最后在 App
包含 ForwardCounter
和 BackwardCounter
两个组件,示例代码如下:
importReactfrom"react"; importBackwardCounterfrom"./components/BackwardCounter"; importForwardCounterfrom"./components/ForwardCounter"; functionApp() { return( <> <ForwardCounter/> <BackwardCounter/> </> ); } export defaultApp;
你可能注意到了,ForwardCounter
与 BackwardCounter
的逻辑几乎一样,唯一的区别就是对状态数组的操作,一个是 +1
和 -1
。因此我们可以把相同的逻辑抽取到一个 Custom hook 里,新建 useCounter
文件,
import { useState, useEffect } from 'react'; const useCounter = (forwards = true) => { const [counter, setCounter] = useState(0); useEffect(() => { const interval = setInterval(() => { if (forwards) { setCounter((prevCounter) => prevCounter + 1); } else { setCounter((prevCounter) => prevCounter - 1); } }, 1000); return () => clearInterval(interval); }, [forwards]); return counter; }; export default useCounter;
从上述代码我们可以注意到,函数返回 counter
这个状态值,此函数通过 forwards
参数进行控制,自定义的计时组件是 +1
计时器 或 -1
计时器。
接下来我们来改写 ForwardCounter
累加计时器,示例代码如下:
import Card from './Card'; import useCounter from '../hooks/use-counter'; const ForwardCounter = () => { const counter = useCounter(); return <Card>{counter}</Card>; }; export default ForwardCounter;
接下来我们继续改写 BackwardCounter
递减的计时器,示例代码如下:
import Card from './Card'; import useCounter from '../hooks/use-counter'; const BackwardCounter = () => { const counter = useCounter(false); return <Card>{counter}</Card>; }; export default BackwardCounter;
到这里,本示例就介绍完了,使用 Custom hook 抽取了相似的逻辑进行复用,让我们的代码简化了不少。
接下来我们来做一个复杂的示例,做一个我们常用的在线表单验证示例,在我们的业务里,我们会经常会用,因此我们们有必要把通用逻辑抽取到 Custom Hooks,方便我们进行复用。
首先我们需要梳理下需求,有哪些通用逻辑需要抽象出来
基于这些简单逻辑,我们编写代码吧,这里我们用到了 useReducer , 让我们的逻辑更加清晰和直观,useInput
的示例代码如下:
import { useReducer } from "react"; const initialInputState = { value:'', isTouched:false } const inputStateReducer = (state,action) => { if(action.type === 'INPUT'){ return {value:action.value,isTouched:state.isTouched} } if(action.type ==='BLUR'){ return {value: state.value,isTouched: true} } if(action.type === 'RESET'){ return {value: '',isTouched:false} } return inputStateReducer; } const useInput = (validateValue)=>{ const [inputState,dispatch]= useReducer( inputStateReducer, initialInputState ); const valueIsValid = validateValue(inputState.value); const hasError = !valueIsValid && inputState.isTouched; const valueChangeHandler = (event)=>{ dispatch({ type: 'INPUT', value: event.target.value }); }; const inputBlurHandler = (event)=>{ dispatch({type:'BLUR'}); }; const reset = () =>{ dispatch({type:'RESET'}) }; return{ value:inputState.value, isValid:valueIsValid, hasError, valueChangeHandler, inputBlurHandler, reset } }; export default useInput;
从上述代码,我们可以看出,通过 useInput(validateValue) 传参的形式,把验证规则进行传入,同时我们定义 valueChangeHandler
,inputBlurHandler
,reset
事件用于更改数据状态,达到验证格式是否正确。
接下来我们来编写表单验证组件,BasicForm
组件,这个组件主要显示三个输入框,分别对应姓、名、邮箱。基于这些输入框,我们要验证非空和邮箱格式是否正确。提交按钮默认会灰色不可点,只有通过验证才能提交按钮。示例的界面如下图所示:
接下来,我们来开始编写代码,首先自定义格式验证函数,代码如下:
//为了示意,只是简单验证,更规范的邮箱验证建议使用正则表达式 const isNotEmpty = (value)=>value.trim() !== ''; const isEmail =(value)=>value.includes('@');
接下来我们需要调用自定义的 Custom hook,将这些自定义验证规则进行传入,并分别定义 Custom hook 返回对应的变量
const { value:firstNameValue, isValid:firstNameIsValid, hasError:firstNameHasError, valueChangeHandler: firstNameChangeHandler, inputBlurHandler: firstNameBlurHandler, reset: resetFirstName, } = useInput(isNotEmpty); const { value: lastNameValue, isValid: lastNameIsValid, hasError: lastNameHasError, valueChangeHandler: lastNameChangeHandler, inputBlurHandler: lastNameBlurHandler, reset: resetLastName, } = useInput(isNotEmpty); const { value: emailValue, isValid: emailIsValid, hasError: emailHasError, valueChangeHandler: emailChangeHandler, inputBlurHandler: emailBlurHandler, reset: resetEmail, } = useInput(isEmail);
接下来我们来定义按钮提交事件,如果没有相关错误,提交按钮的状态为可点,并将表单的内容置换为空,示例代码如下:
let formIsValid = false; if(firstNameIsValid&&lastNameIsValid&&emailIsValid){ formIsValid = true; } const submitHandler = event =>{ event.preventDefault(); if(!formIsValid){ return; } console.log('Submitted!'); console.log(firstNameValue, lastNameValue, emailValue); resetFirstName(); resetLastName(); resetEmail(); } const firstNameClasses = firstNameHasError ? 'form-control invalid' : 'form-control'; const lastNameClasses = lastNameHasError ? 'form-control invalid' : 'form-control'; const emailClasses = emailHasError ? 'form-control invalid' : 'form-control';
最后我们来定义组件UI,用上上述代码相关的事件变量和样式变量,示例代码如下:
<form onSubmit={submitHandler}> <div className="control-group"> <div className={firstNameClasses}> <label htmlFor='name'>First Name</label> <input type="text" id="name" value={firstNameValue} onChange={firstNameChangeHandler} onBlur={firstNameBlurHandler} /> {firstNameHasError && <p className="error-text">Please enter a first name.</p>} </div> <div className={lastNameClasses}> <label htmlFor='name'>Last Name</label> <input type='text' id='name' value={lastNameValue} onChange={lastNameChangeHandler} onBlur={lastNameBlurHandler} /> {lastNameHasError && <p className="error-text">Please enter a last name.</p>} </div> </div> <div className={emailClasses}> <label htmlFor='name'>E-Mail Address</label> <input type='text' id='name' value={emailValue} onChange={emailChangeHandler} onBlur={emailBlurHandler} /> {emailHasError && <p className="error-text">Please enter a valid email address.</p>} </div> <div className="form-action"> <button disabled={!formIsValid}> Submit </button> </div> </form>
到这里一个复杂的表单格式验证示例就介绍完了,你是不是体会到了 Custom hook 的好处了。
接口请求的场景也是十分常见的,如果使用 Custom hook ,我们该如何做呢?
首先我们新建 useAxios 自定 hook ,定义 isLoading
和 error
两个数据状态标识数据请求状态和接口是否请求成功状态,同时定义了 sendRequest
用来对请求 api 的数据处理,相关代码结构如下:
import { useState, useCallback } from "react"; import axios from "axios"; axios.defaults.baseURL = "https://jsonplaceholder.typicode.com"; const useAxios = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const sendRequest = useCallback(() => {}, []); return { isLoading, error, sendRequest, }; }; export default useAxios;
在 sendRequest
函数里,有 requestConfig 和 applyData 两个参数,通过 requestConfig 我们可以配置接口请求的地址、请求方式和数据请求内容等。最后通过 applyData 处理 api 返回的数据内容。
import { useState, useCallback } from 'react'; import axios from 'axios'; axios.defaults.baseURL = "https://qianduandaren.com"; const useAxios = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const sendRequest = useCallback(async (requestConfig, applyData) => { setIsLoading(true); setError(null); let res; try { res = await axios({ url: requestConfig.url, method: requestConfig.method ? requestConfig.method : 'GET', headers: requestConfig.headers ? requestConfig.headers : {}, data: requestConfig.data ? requestConfig.data : null, }); } catch (error) { setError(error.message || 'Something went wrong!'); } finally { if (res) { applyData(res.data); } setIsLoading(false); } }, []); return { isLoading, error, sendRequest, }; }; export default useAxios;
接下来我们在项目中调用自定义的 useAxios hook,示例代码如下:
import { useEffect, useState } from "react"; import useAxios from "./useAxios"; export default function App() { const [data, setData] = useState([]); const { isLoading, error, sendRequest: fetchData } = useAxios(); const { sendRequest: createData } = useAxios(); useEffect(() => { fetchData({ url: "/posts" }, (res) => { console.log(res); setData(res); }); }, []); const clickNewData = () => { createData( { url: "/posts", method: "POST", data: { // id 沒加是因為 JSONPlaceholder 只做假新增不會更新到資料庫,所以 JSONPlaceholder 會自動產生一個假的 id userId: 10, title: "新增的物件", body: "example" } }, (res) => console.log(res) ); }; if (isLoading) return <h1>Loading...</h1>; if (error) return <h1>{error}</h1>; return ( <> <ul> {data.map((item) => ( <li key={item.id}> <p>{item.title}</p> </li> ))} </ul> <button onClick={clickNewData}>新增</button> </> ); }
关于 Custom Hooks 介绍就到这里,通过前面的几个例子,我们应该有了清晰认识,那我们为什么用 Custom Hooks 呢?简单的总结下,就是将重复的代码逻辑抽取出来,减少重复的代码,提升代码的可读性。
注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载