首页与我联系

「React Hooks 学习笔记」关于Custom Hooks 的使用介绍(八)

By 前端达人
Published in 4-React
October 28, 2022
1 min read
「React Hooks 学习笔记」关于Custom Hooks 的使用介绍(八)

React Hook 系列文章

  1. State Hook 的使用介绍(一)
  2. useEffect Hook 的使用介绍(二)
  3. React.memo 的使用介绍(三)
  4. useMemo 和 useCallback 的使用介绍(四)
  5. useRef的使用介绍(五)
  6. useContext 的使用介绍(六)
  7. useReducer 的使用介绍(七)

本篇文章内容

  1. 什么是 Custom Hooks?
  2. 使用 Custom Hooks 需要知道的事
  3. 一个简单的示例
  4. 一个复杂的表单示例
  5. Axios 接口请求示例
  6. Custom Hooks 的使用总结

什么是 Custom Hooks?

经过前面几篇文章的介绍,我们学习了许多常使用的 hooks,不过除了那些 hooks 之外,我们也可以将一些常用的逻辑抽取出来封装到一个函数里,这就是 Custom Hook。

使用 Custom Hooks 需要知道的事

  • 官方建议使用 Custom hook 必须以 use 作为函式名称的开头,让 eslint-plugin-react-hooks 去检查这个 hook 是否有违反 react hook 的规则。
  • 可以在 Custom hook 里面使用其他的 React hook。

一个简单的示例

在了解什么是 Custom Hook 后,我们还是通过一个计时器案例快速熟悉下 Custom Hook 。这个案例比较简单,界面包含两个计时器,1个累加的计时器,1个相减的计时器,如下图所示。

截屏2022-10-27 23.46.39.png

未使用 Custom hook 编写

在使用 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 包含 ForwardCounterBackwardCounter 两个组件,示例代码如下:

importReactfrom"react";
importBackwardCounterfrom"./components/BackwardCounter";
importForwardCounterfrom"./components/ForwardCounter";

functionApp() {
return(
    <>
      <ForwardCounter/>
      <BackwardCounter/>
    </>
  );
}

export defaultApp;

使用 Custom hook 编写

你可能注意到了,ForwardCounterBackwardCounter 的逻辑几乎一样,唯一的区别就是对状态数组的操作,一个是 +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) 传参的形式,把验证规则进行传入,同时我们定义 valueChangeHandlerinputBlurHandlerreset 事件用于更改数据状态,达到验证格式是否正确。

接下来我们来编写表单验证组件,BasicForm 组件,这个组件主要显示三个输入框,分别对应姓、名、邮箱。基于这些输入框,我们要验证非空和邮箱格式是否正确。提交按钮默认会灰色不可点,只有通过验证才能提交按钮。示例的界面如下图所示:

截屏2022-10-29 19.43.11.png

接下来,我们来开始编写代码,首先自定义格式验证函数,代码如下:

//为了示意,只是简单验证,更规范的邮箱验证建议使用正则表达式
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 的好处了。

Axios 接口请求示例

接口请求的场景也是十分常见的,如果使用 Custom hook ,我们该如何做呢?

建立基本结构

首先我们新建 useAxios 自定 hook ,定义 isLoadingerror 两个数据状态标识数据请求状态和接口是否请求成功状态,同时定义了 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 函数

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

接下来我们在项目中调用自定义的 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 介绍就到这里,通过前面的几个例子,我们应该有了清晰认识,那我们为什么用 Custom Hooks 呢?简单的总结下,就是将重复的代码逻辑抽取出来,减少重复的代码,提升代码的可读性。

前端达人公众号.jpg

注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载


Tags

hookreact
Previous Article
「短文」如何使用 Vue 的 Composition API 创建 Computed 属性
前端达人

前端达人

专注前端知识分享

Table Of Contents

1
React Hook 系列文章
2
本篇文章内容
3
什么是 Custom Hooks?
4
使用 Custom Hooks 需要知道的事
5
一个简单的示例
6
一个复杂的表单验证示例
7
Axios 接口请求示例
8
Custom Hooks 的使用总结

相关文章

「React Hooks 学习笔记」关于useReducer 的使用介绍(七)
October 14, 2022
1 min

前端站点

VUE官网React官网TypeScript官网

公众号:前端达人

前端达人公众号