react hooks从入门到不离不弃

206 阅读13分钟

前面在学vue的时候写了一篇自定义指令从入门到不离不弃,现在写vue已经有四五个月了,发现对自己之前用的react倒有点生疏了,也就才几个月没用,写代码这事真的是不能停,所以趁着还没完全忘记,赶紧捡起来自己的老本行,来一篇react hooks从入门到不离不弃。talk is cheap, show me the code。

什么是hooks

  • hooks 是 React 16.8 新增的特性,丰富扩展了原有函数组件的功能,让函数组件也有了类组件的一些特性。

hooks解决了什么问题

类组件的缺点

1. this指向问题
  • 例如每次声明函数都需要手动的去绑定 this ,想要获取 state 和 props 还需要通过 this.state.xxx 和 this.props.xxx 来获取,代码不够简洁
2. 代码复杂,难以组织
  • 数据获取时,需要在组件挂载 (componentDidMount) 和组件更新 (componentDidUpdate) 阶段分别去获取,不够统一
  • 需要事件监听时,需要在需要在组件挂载 (componentDidMount) 时注册事件,在和组件卸载 (componentWillUnmount) 时卸载事件,这种分散的写法很容易遗漏导致忘记卸载事件。
3. 组件之间状态复用困难
  • 类组件中的状态都是通过 state 定义在组件内部没办法抽离的,如果其他组件也需要用到相同的 state 则需要重定义一份
  • react 本身为单向数据流,状态只能从父组件传递到子组件中,如果需要子组件修改父组件的状态,则需要用到 Hoc(高阶组件) 或者 render props(渲染属性) 来解决,这样都会造成组件变得复杂

hooks如何解决的

1. this指向问题
  • 可以把函数组件理解成普通的函数,我们在函数中定义的变量和函数是可以直接访问的,而不需要用 this 去访问,因此也不用关心this的指向
2.代码复杂,难以组织
  • react 中内置的 useEffect 钩子,可以实现类组件中的 componentDidMount、componentDidUpdate、componentWillUnmount 三个生命周期的作用,所以我们就可以将事件注册和卸载以及数据获取统一起来管理,解决之前存在的分散的问题。
3. 组件之间状态复用困难
  • react hook 除了内置的几个以外,是支持自定义 hook 的,我们可以将一些公用的状态处理逻辑单独封装成一个 hook,在多个组件中可以共用,这样就解决了类组件中的状态复用的问题。

除了以上的优点外,hook 还有其他的优点,可以用来实现一些类组件实现不了的功能,比如可以用 hooks 实现一个简单的全局数据管理的功能,代替 redux。

常见hook有哪些

接下来我们就来一个一个地讲解react中常用的一些hook

useState

useState 是用来解决函数组件中不能定义自己的状态的问题,useState 可以传递一个参数,做为状态的初始值,返回一个数组,数组的第一个元素是返回的状态变量,第二个是修改状态变量的函数。

const [state, setState] = useState(initalState); // 初始化,state可以任意命名
// ...
setState(newState); // 修改state的值

在类组件中是通过 this.setState 来修改类组件中的状态值的,函数组件中则通过 useState 来修改,不同的是 this.setState 是合并修改,但是 useState 中则是直接替换。

示例:
import { useState } from 'react';

function Demo() {
  const [count, setCount] = useState(0);
  
  const add = () => {
    setCount(count + 1);
  }

  return (
    <div>
    	<button onClick={add}>+1</button>
        <p>{`count: ${count}`}</p>
    </div>
  );
}

export default Demo;

如果这段代码用类组件写的话该如何写呢?

import React from 'react';

class Demo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    this.add = this.add.bind(this);
  }

  add() {
    this.setState({
      count: this.state.count + 1
    })
  }

  render () {
    return (
      <div>
        <button onClick={this.add}>add1</button>
 	<p>{`count: ${this.state.count}`}</p>
      </div>
    );
  }
}

export default Demo;
高级用法
  1. 思考:这样可以实现对count加3的操作吗

    import { useState } from 'react';
    
    function Demo() {
      const [count, setCount] = useState(0);
      
      const add = () => {
        for (let i = 0; i < 3; i ++) {
          setCount(count + 1);
        }
        // for (let i = 0; i < 3; i ++) {
        //   setCount(prev => prev + 1);
        // }
      }
    
      return (
        <div>
        	<button onClick={add}>+3</button>
    	<p>{`count: ${count}`}</p>
        </div>
      );
    }
    
    export default Demo;
    

    答案:不能实现

    原因:在类组件中 this.setState 是异步执行的,同样 useState 修改状态也是异步的,虽然 setCount 执行了三次,但是因为每次执行并不是立即生效的,所以每次拿到的 count 的值其实是一样的,都是初始值,所以其实相当于是就执行了1次。

  2. 思考:alert的时候能拿到最新的值吗

    import { useState } from 'react';
    
    function Demo(){
      let [count, setCount] = useState(0);
      
      const alertCount = () => {
        setTimeout(() => {
          alert(count);
        }, 3000);
      }
      
      const add = () => {
        setCount(count + 1);
      }
      
      return (
      	<div>
             <button onClick={add}>+1</button>
             <button onClick={alertCount}>alert</button>
             <p>{`count: ${count}`}</p>
    	</div>
      )
    }
    

    答案:不能

    原因:函数组件在每次渲染的时候都会形成一个闭包,都有自己的 state 和 props 以及 事件处理函数,每次点击 add 时都会重新渲染,这个时候执行 alert 拿到的是那一次渲染的值,之后再更新 count 是不会对当时的状态有影响的。

useEffect

我们写的很多组件都会产生很多的副作用,比如 ajax 请求,因为要与服务端通信,可能会更新缓存,保存日志等,还有操作 dom 也算是副作用,这些副作用在类组件中都是放在 componentDidMount,componentDidUpdate 中执行的,在 hook 中可以用 useEffect 来处理副作用。

useEffect 可以传入2个参数,第1个参数为我们定义的执行函数、第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect,那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的。

useEffect(() => {
  // 此处编写 组件挂载之后和组件重新渲染之后执行的代码
  ...

  return () => {
    // 此处编写 组件即将被卸载前执行的代码
    ...
  }
}, [dep1, dep2 ...]); // 依赖数组
  1. 第一个参数中的代码是组件挂载和更新就会执行的代码
  2. return 出去的代码会在组件卸载时才会执行
  3. 依赖数组不是必填项,如果不传则每次渲染都会去执行,传值的话在依赖项发生改变时函数中的代码才会执行,如果传空数组则会在组件第一次挂载才会执行
示例:
import { useState, useEffect } from 'react';

function Demo() {
  const [count, setCount] = useState(0);
  const [a, setA] = useState(0);
  
  useEffect(() => { // 实现componentDidMount、componentDidUpdate、componentWillUnmount
    console.log('---effect 执行---');
    document.title = `被点击了${count}次`;
  })
  
  const add = () => {
    setCount(count + 1);
  }
  
  const addA = () => {
    setA(a + 1);
  }

  return (
    <div>
    	<button onClick={add}>add</button>
        <button onClick={addA}>addA</button>
        <p>{`count: ${count}`}</p>
        <p>{`a: ${a}`}</p>
    </div>
   );
}

export default Demo;
清除副作用
import { useState, useEffect } from 'react';

function Demo() {
  const [count, setCount] = useState(0);
  const [a, setA] = useState(a);

  useEffect(() => {
    // 组件挂载时注册计数器
    let timer = setInterval(() => {
      setA(a+1)
    }, 1000);
    
    // 组件卸载时卸载计时器
    return () => {
    	clearInterval(timer);
    }
  }, []);

  useEffect(() => {
    // 修改页面标题
    document.title = `被点击了${count}次`;
  }, [count]);
  
  const addCount = () => {
    setCount(count + 1);
  }
  
  return (
    <div>
    	<button onClick={addCount}>addCount</button>
        <p>{`count: ${count}`}</p>
        <p>{`a: ${a}`}</p>
    </div>
  );
}

export default Component;
useLayoutEffect

useLayoutEffect 使用方法、所传参数和 useEffect 完全相同。大多数情况下将 useEffect 替换成 useLayoutEffect 完全看不出区别,那两个函数的区别在哪呢?

示例:
import { useState, useEffect, useLayoutEffect } from 'react'

function Demo() {
  const [str, setStr] = useState('hello world');

  useEffect(() => {
    let i = 0;
    while(i < 100000000) {
      i ++;
    }
    setStr('world hello')
  }, []);

  useLayoutEffect(() => {
    let i = 0;
    while(i < 100000000) {
      i ++;
    }
    setStr('world hello')
  }, []);

  return (
    <div>
	<p>{`str: ${str}`}</p>
    </div>
  )
}

可以发现使用 useEffect 时,页面挂载会出现闪烁,而使用 useLayoutEffect 时页面没有闪烁,是因为 useEffect 是在页面渲染完成后再去更新 str 的,所以会出现短暂的闪烁,而 useLayoutEffect 是在页面还没有渲染时就将 str 给更新了,所以没有出现闪烁。为什么会出现这种情况呢?

原因:
  • useEffect 是在 dom 渲染到页面后才会去执行,useLayoutEffect 则是在 dom 变化渲染到页面之前执行的
  • 如果有操作 dom 的逻辑尽量放在 useLayoutEffect 中,因为如果放在 useEffect 中的话会重新触发浏览器进行回流,重绘
  • useEffect 能覆盖大部分场景,因为 useLayoutEffect 会阻塞渲染,所以需要小心的使用

useMemo

useMemo 是为了减少组件重新渲染时不必要的函数计算,可以用来做性能优化

const memoizedValue = useMemo(() => {
  // 计算逻辑
  ...
  // return res;
}, [a, b]);

useMemo 可以传入2个参数,第1个参数为函数,用来进行一些计算,第2个参数是依赖关系(可选参数),返回值为第一个函数 return 出去的值,只有在依赖项发生变化时才会重新执行计算函数进行计算,如果不传依赖项,每次组件渲染都会重新进行计算。

示例:
import { useState, useMemo } from 'react'

function Demo() {
  const [num, setNum] = useState(2022);
  const [count, setCount] = useState(0);

  const addCount = () => {
    // count更新并不会出发 useMemo 重新计算
    setCount(count + 1);
  }
  
  const addNum = () => {
    // num 更新并不会出发 useMemo 重新计算
    setNum(num + 100);
  }
  
  const total = useMemo(() => {
    console.log('---求和---');
    // 求和计算
    let temp = 0;
    for(let i = num; i > 0; i--) {
      temp += i;
    }
    return temp;
  }, [num]);
  
  return (
    <div>
    	<button onClick={addCount}>addCount</button>
        <button onClick={addNum}>addNum</button>
        <p>{`count: ${count}`}</p>
        <p>{`num: ${num}`}</p>
        <p>{`total: ${total}`}</p>
    </div>
  )
}
  1. 点击修改 count 的值会引发组件重新渲染,但是 total 对应的计算函数却不需要重新计算一遍。

  2. 点击修改num的值,total 对应的计算函数肯定会重新执行一遍,因为num是该计算函数的依赖。

useCallback

返回一个缓存的回调函数。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

useCallback 的用法和 useMemo 完全一样,useMemo 返回的是计算函数 return 出去的值,而 useCallback 可以理解成返回的是那个计算函数。

示例:
import { useState, useCallback } from 'react'

function Demo() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  const clickHanlder = () => {
    setCount(count + 1);
  };
  
  // const clickHanlder = useCallback(() => {
  //   setCount(count + 1);
  // }, [count]);
  
  const addNum = () => {
    setNum(num + 1);
  }

  return (
    <div>
        <Button clickHanlder={clickHanlder}></Button>
        <button onClick={addNum}>addNum</button>
    </div>
  )
}

const Button = (props) => {
  const { clickHanlder } = props;
  
  console.log('---render---');
  
  return (
    <button onClick={clickHanlder}>{'addCount'}</button>
  );
}
React.memo()

类组件中有 shouldComponentUpdate 和 PureComponent 来避免子组件做不必要的渲染,shouldComponentUpdate 接收两个参数,nextProps 和 nextState,可以用来跟 this.props 和 this.state 进行比较,返回 true 表示需要更新,返回 false 表示可以跳过更新。PureComponent 则会自己对 props 和 state 做一个浅比较来决定要不要更新。

函数组件中的 React.memo() 也有类似的功能,它和 PureComponent 类似,但是只适用于函数组件,默认情况下仅对 props 进行一个浅比较来决定要不要更新,复杂情况下支持自己写对比的逻辑。

function Demo(props) {
  // ...
}

function compare(prevProps, nextProps) {
  // 自己写对比逻辑,返回 true 更新,false 跳过更新
  // return false
}

export default React.memo(Demo, compare)
const Button = React.memo((props) => {
  const { label, clickHanlder } = props;
  
  console.log('---render---');
  
  return (
    <button onClick={clickHanlder}>{label}</button>
  );
});

useRef

useRef 可以帮助我们获取 dom 和 react 组件实例,类组件中的 React.createRef() 也有相同的功能。

const xxxRef = useRef(initialValue);
// 使用 xxxRef.current 获取引用的值
示例:
import { useState, useRef } from 'react'

function Demo() {
  const inputRef = useRef();

  const handleFocus = () => {
    // document.getElementById('my-input').focus();
    inputRef.current.value = 'focus';
    inputRef.current.foucs();
  }
  
  const handleBlur = () => {
    // document.getElementById('my-input').blur();
    inputRef.current.value = 'blur';
    inputRef.current.blur();
  }
  
  return (
    <div>
        <input ref={inputRef} id={'my-input'} />
        <button onClick={handleFocus}>focus</button>
        <button onClick={handleBlur}>blur</button>
    </div>
  )
}

除了用 useRef 获取组件实例,还可以用来存储变量的值,但是需要注意的一点是,修改 .current 的值不会触发组件的重新渲染,请看下面示例:

import { useState, useRef } from 'react'

function Demo() {
  const countRef = useRef(0);
  const [num, setNum] = useState(0);

  const addCount = () => {
    // 使用 useRef 去更新值并不会出发组件渲染
    countRef.current = countRef.current + 1;
  }
  
  const alertCount = () => {
    setTimeout(() => {
      alert(countRef.current);
    }, 3000);
  }
  
  const addNum = () => {
    // 使用 useState 去更新会触发组件渲染
    setNum(num + 1);
  }
  
  return (
    <div>
        <button onClick={addCount}>addCount</button>
        <button onClick={addNum}>addNum</button>
        <button onClick={alertCount}>alert</button>
        <p>{`count: ${countRef.current}`}</p>
        <p>{`num: ${num}`}</p>
    </div>
  )
}
forwardRef

思考下我们在父组件中想给子组件传递 ref 可以实现吗?

import { useRef, forwardRef } from 'react';

function Child(props) {
  const { ref } = props; // 这样是不可以的
  return (
    <input ref={ref} />
  );
}

function Parent() {
  const childRef = useRef();
  
  const onFocus = () => {
    childRef.current.focus();
  }
  
  return (
    <div>
    	<Child ref={childRef} />
        <button onClick={onFocus}>focus</button>
    </div>
  );
}

forwardRef 可以在父组件中操作子组件的 ref 对象,并且将 ref 对象作为一个参数传递给了子组件。

useImperativeHandle

React 中的数据流是单向的,父组件可以将函数和状态传递给子组件,这样子组件内就可以使用父组件中的变量和函数了,如果我们想在父组件中使用子组件内定义的函数和状态呢?这就需要用到 useImperativeHandle 这个 hook 了,使用 useImperativeHandle 的时候还需要结合上面讲的 useRef 和 forwardRef 来结合使用,具体使用步骤如下:

  1. 在父组件中使用 useRef 创建 ref 引用变量
  2. 使用 forwardRef 将创建的 ref 引用传递到子组件中去
  3. 将子组件中的函数和状态通过 useImperativeHandle 挂载到传递过来的 ref 对象上
// ref 为父组件传过来的 ref 引用
useImperativeHandle(ref, () => {
	// return出去的属性都挂载在了父组件传过来的ref对象上,若父组件需要调用子组件内的 xxx函数,则通过 ref.current.xxx()调用
  return {
    xxx: xxx
  }
}, [deps]) // deps 为依赖数组,可选项
示例:
import { forwardRef, useState, useImperativeHandle, useRef } from 'react';

function Child(props, ref) {
  const [count, setCount] = useState(0);
  
  useImperativeHandle(ref, () => {
    return {
      addCount: () => {
        setCount(count + 1);
      }
    }
  })
  
  return (
    <div>
    	<p>{`count: ${count}`}</p>
    </div>
  );
}

Child = forwardRef(Child);

function Parent() {
    const childRef = useRef();
    return (
        <Child ref={childRef} />
        <button
          onClick={() => {
            childRef.current.addCount();
          }}
        >add</button>
    );
}

useContext

在 React 中传递属性只能一层一层传,如果组件结构比较复杂,层级比较深的时候,数据传递起来就比较麻烦,可能会经过很多次的传递才能将属性传递到目标组件中,那么有没有一种可以在全局进行状态共享的实现方法呢?useContext 就是为了解决这个问题的,可以实现不必层层传递就能共享状态的功能。

示例:
// context.js
import React from 'react';
// 全局注册
const UserContext = React.createContext({ userName: '张三', age: 24 });
export default UserContext;

// Parent.js
import React from 'react';
import UserContext from './context';

function Parent() {
  return (
    <UserContext.Provider value={{ name: '李四', age: 28 }}>
        <div>
            <Child />
        </div>
    </UserContext.Provider>
  );
}

// Child.js
function Child() {
  // Child 组件没有用到 context 中的状态,不用做任何处理
  return (
    <div>
    	<GrandSon />
    </div>
  );
}

// GrandSon.js
import UserContext from './context';
import { useContext } from 'react';

function GrandSon() {
  // 用 useContext 引入传递的状态
  const userContext = useContext(UserContext);
  
  return (
    <div>
    	<p>{`name: ${userContext.name}`}</p>
    	<p>{`age: ${userContext.age}`}</p>
    </div>
  );
}

如果全局有多个共享的 context 在子组件中如何使用呢?请看下面示例

import React, { useContext } from 'react'

const UserContext = React.createContext();
const JobContext = React.createContext();

function Parent() {
  return (
    <UserContext.Provider value={{ name: '张三' }}>
        <JobContext.Provider value={{ position: '前端开发' }}>
            <Child />
        </JobContext.Provider>
    </UserContext.Provider>
  )
}

function Child() {
  const user = useContext(UserContext);
  const job = useContext(JobContext);
  return (
    <div>
    	<p>{`name: ${user.name}`}</p>
    	<p>{`job: ${job.position}`}</p>
    </div>
  )
}

export default AppComponent;

useReducer

useReducer 也是用来实现状态管理的 hook,useState 就是基于 useReducer 实现的,useReducer 可以实现比 useState 更复杂的状态管理逻辑。接下来就看看如何去使用 useReducer 的。

import React, { useReducer } from 'react';

// 1.需要有一个 reducer 函数,第一个参数为之前的状态,第二个参数为行为信息
function reducer(state, action) {
  switch (action) {
    case 'add':
      return state + 1;
    case 'minus':
      return state - 1;
    default:
      return state;
  }
}

function Demo() {
  // 2.引入useReducer,第一个参数时上面定义的reducer,第二个参数时初始值
  // 3.返回为一个数组,第一项为状态值,第二项为一个 dispatch 函数,用来修改状态值
  const [count, setCount] = useReducer(reducer, 0);
  // const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => { setCount({'add'}) }} >add</button> | 
      <button onClick={() => { dispatch('minus') }} >minus</button> | 
      <button onClick={() => { dispatch('unknown') }} >unknown</button>
      <p>{`count: ${count}`}</p>
    </div>
  );
}

思考:如果 state 是一个对象在 reducer 怎么处理?dispath 修改 state 时支持传参吗?接下来看看比较复杂的场景该如何实现。

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'addAge': // 修改年龄
      return { ...state, age: state.age + 1 };
    case 'updateName': // 修改名字
      return { ...state, name: action.newName };
    default:
      return state;
  }
}

const initalData = { name: '张三', age: 24 };

function Demo() {
  const [person, dispatch] = useReducer(reducer, initalData);

  return (
    <div>
      <button onClick={() => { dispatch({ type: 'addAge' }) }} >addAge</button> | 
      <button onClick={() => { dispatch({ type: 'updateName', newName: '李四' }) }} >updateName</button> | 
      <button onClick={() => { dispatch('unknown') }} >unknown</button>
      <p>{`name: ${person.name}`}</p>
      <p>{`age: ${person.age}`}</p>
    </div>
  );
}
用 useReducer 和 useContext 实现全局的状态管理

我们在学习 useContext 时实现了全局的状态共享逻辑,实现了在父组件中定义的状态在任何子孙组件中都可以获取并使用,但是并没有去实现修改共享状态的逻辑。而 useReducer 可以实现状态的修改逻辑,所以将 useContext 和 useReducer 结合起来是不是就可以实现对全局状态的管理,既可以获取全局状态,又可以修改全局状态,接下来就看看如何实现的

  • 用 useContext 获取全局状态
  • 用 useReducer 修改全局状态
  1. 用 React.createContext() 定义一个全局对象

    import React from 'react';
    const UserContext = React.createContext();
    export default UserContext;
    
  2. 定义 reducer 函数

    export default function reducer(state, action) {
      switch (action.type) {
        case 'login': // 登录
          return { isLogin: true, ...action.user };
        case 'logout': // 登出
          return { isLogin: false };
        case 'update': // 修改用户信息
          return { ...state, ...action.user };
        default:
          return state;
      }
    }
    
  3. 父组件中注入全局状态

    import { useReducer, useContext } from 'react';
    import UserContext from './context.js';
    import reducer from './reducer.js';
    
    function Parent() {
      // 用 useReducer 生成一个全局的用户信息对象和 dispatch 函数
      const [state, dispath] = useReducer(reducer, { isLogin: false });
      
      return (
        <UserContext.Provider value={{ userInfo: state, dispatch }}>
        	<Login />
        	<Logout />
        	<Update />
        	<User />
        </UserContext>
      );
    }
    
  4. 在登录,登出和修改用户信息的组件中分别派发不同的事件

    function Login() {
      // 获取父组件中注入的全局状态
      const state = useContext(UserContext);
      
      const login = () => {
        // 因为 useReducer 返回的 dispatch 以经在父组件中被注入了,所以这里可以直接拿来用
        state.dispatch({ // dispatch 派发登录行为
          type: 'login',
          user: { name: '张三', mobile: '123456' }
        });
      }
      
      return (
        <button onClick={login}>登录</button>
      );
    }
    
    function Logout() {
      const state = useContext(UserContext);
      
      const logout = () => {
        state.dispatch({ type: 'logout' }); // dispatch 派发登出行为
      }
      
      return (
        <button onClick={logout}>登出</button>
      );
    }
    
    function Update() {
      const state = useContext(UserContext);
      
      const update = () => {
        state.dispatch({ // dispatch 派发修改用户信息的行为
          type: 'update',
          user: { age: 25 }
        });
      }
      
      return (
        <button onClick={update}>修改信息</button>
      );
    }
    
  5. 在 User 组件中展示用户信息

    function User() {
      // 使用 useContext 获取全局对象
      const state = useContext(UserContext);
      const { userInfo } = state;
      
      return (
          <div>
             {
               userInfo.isLogin ? <>
                 <p>{`name: ${userInfo.name}`}</p>
                 <p>{`age: ${userInfo.age}`}</p>
               </> : <>
                 <p>未登录</>
               </>
             }
          </div>
      );
    }
    

总结:中心思想还是刚才讲的两点,使用 useReducer 去修改全局状态,使用 useContext 去获取全局状态。

这样,我们不管在登录组件 、登出组件还是修改用户信息组件中通过 dispatch 派发事件去修改全局的 userInfo 时,都能在 user 组件中监听到状态的变化,从而展示不通的内容,这样就实现了组件之间的状态共享和修改逻辑,至此,一个简单的状态管理功能就实现了。

思考:既然 useState 和 useReducer 都能实现组件的状态管理,那么我们该怎么选择呢?

答案:组件自己内部的状态用 useState 就可以了,如果设计到组件之间的状态共享可以考虑使用 useReducer

如何自定义hook

React 规定所有的 hook 都要以 use 开头,hook 其实就是一个函数,也支持传参,支持返回值,也可以没有返回值,而且在我们自己定义的 hook 中也可以用已经实现的 hook,接下来我们就自己动手实现两个简单的 hook。

1. useTitle -- 给网页设置标题的 hook

假设有一个场景,我们需要在某些页面上设置特殊的 title,在其他页面上再将 title 恢复成原来的,该怎么实现呢?

首先,我们的 hook 肯定得有一个参数,这个参数就是我们要设置的 title,其次判断要不要有返回值,因为我们只需要将 document.title 给改成我们想要的就行,不用接收返回,所以这个 hook 是不需要返回值的,接下来就看看代码是怎么实现的。

import { useEffect, useRef } from 'react';

function useTitle(title) {
  const defaultTitle = useRef(document.title);
  
  useEffect(() => {
    document.title = title;
    
    return () => {
      document.title = defaultTitle.current;
    }
  }, [title]);
  // return 不需要返回值
}
2. useCookie -- 操作 cookie

一般在登录某个应用的时候都会将用户的 id 或者其他信息存储到 cookie 中去,注销登录的时候将 cookie 给清除,我们就可以自定义一个操作 cookie 的 hook,帮助我们保存和修改 cookie。

React 自带的 useState 也是实现对状态的更改获取功能的,useCookie 也是类似,我们也要实现更改存储和获取cookie,所以仿照着 useState,useCookie 应该也有一个参数,这个参数我们要操作的 cookie 的 key,因为 cookie 有过期时间,以及生效的 domain 等配置,所以还应该有一个配置的参数。返回的也是一个数组,数组的第一项是这个 cookie 的值,第二项是操作 cookie 的方法。

const [userId, setUserId] = useCookie('userId', options);

我们要修改 userId 这个 cookie 的值时,只需要用 setUserId 即可,接下来就来一步一步实现吧。

import { setState } from 'react';
import Cookies from 'js-cookie';

function useCookie(key, options) {
  const [state, setState] = useState(() => {
    const cookieValue = Cookies.get(cookieKey);
    if (isString(cookieValue)) return cookieValue;
    return options.defaultValue;
  });
  
  const updateCookie = (newValue) => {
    setState((prevState) => {
      // 不传 value 就默认移除 cookie
      if (newValue === undefined) {
        Cookies.remove(cookieKey);
      } else {
        Cookies.set(cookieKey, newValue, options);
      }
      return newValue;
    });
  }
}
推荐的 hook 库
  1. ahooks.gitee.io/zh-CN

  2. github.com/streamich/r…

总结

以上就是react中常用的一些hooks,学会了这几个基本上日常开发react项目就基本没有啥问题。在回顾的过程中也悟出了一个道理,编程编程最重要的点在于“编”,只有多“编”,才能让自己写代码的水平提高,几个月不写,之前闭着眼都会用的技术也能忘个七七八八,新掌握的技术几天不看,再次翻开还是跟新的一样,完全不会。所以,代码不能停,coding is never stop!