10分钟教你手写8个常用的自定义hooks

徐小夕 等级 746 1 2

前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。本文是一篇以实战为主的文章,主要讲解实际项目中如何使用hooks以及一些最佳实践,不会一步步再介绍一遍react hooks的由来和基本使用,因为写hooks的文章很多,而且官网对于react hooks的介绍也很详细,所以大家不熟悉的可以看一遍官网

你将收获

  • react hooks核心API使用以及注意事项
  • 实现一个小型redux
  • 实现自定义的useState
  • 实现自定义的useDebounce
  • 实现自定义的useThrottle
  • 实现自定义useTitle
  • 实现自定义的useUpdate
  • 实现自定义的useScroll
  • 实现自定义的useMouse
  • 实现自定义的createBreakpoint

正文

1. react hooks核心API使用注意事项

笔者在项目中常用的hooks主要有useState, useEffect,useCallback,useMemo,useRef。当然像useReducer, useContext, createContext这些钩子在H5游戏中也会使用,因为不需要维护错综复杂的状态,所以我们完全可以由上述三个api构建一个自己的小型redux(后面会介绍如何实现小型的redux)来处理全局状态,但是对于企业复杂项目来说,我们使用redux及其生态会更加高效一些。

我们在使用hooks和函数组件编写我们的组件时,第一个要考虑的就是渲染性能,我们知道如果在不做任何处理时,我们在函数组件中使用setState都会导致组件内部重新渲染,一个比较典型的场景: 10分钟教你手写8个常用的自定义hooks 当我们在容器组件手动更新了任何state时,容器内部的各个子组件都会重新渲染,为了避免这种情况出现,我们一般都会使用memo将函数组件包裹,来达到class组件的pureComponent的效果:

import React, { memo, useState, useEffect } from 'react'
const A = (props) => {
  console.log('A1')
  useEffect(() => {
    console.log('A2')
  })
  return <div>A</div>
}

const B = memo((props) => {
  console.log('B1')
  useEffect(() => {
    console.log('B2')
  })
  return <div>B</div>
})

const Home = (props) => {
  const [a, setA] = useState(0)
  useEffect(() => {
    console.log('start')
    setA(1)
  }, [])
  return <div><A n={a} /><B /></div>
}

当我们将B用memo包裹后,状态a的更新将不会导致B组件重新渲染。其实仅仅优化这一点还远远不够的,比如说我们子组件用到了容器组件的某个引用类型的变量或者函数,那么当容器内部的state更新之后,这些变量和函数都会重新赋值,这样就会导致即使子组件使用了memo包裹也还是会重新渲染,那么这个时候我们就需要使用useMemouseCallback了。

useMemo可以帮我们将变量缓存起来,useCallback可以缓存回调函数,它们的第二个参数和useEffect一样,是一个依赖项数组,通过配置依赖项数组来决定是否更新。

import React, { memo, useState, useEffect, useMemo } from 'react'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  useEffect(() => {
    setA(1)
  }, [])

  const add = useCallback(() => {
    console.log('b', b)
  }, [b])

  const name = useMemo(() => {
    return b + 'xuxi'
  }, [b])
  return <div><A n={a} /><B add={add} name={name} /></div>
}

此时a更新后B组件不会再重新渲染。以上几个优化步骤主要是用来优化组件的渲染性能,我们平时还会涉及到获取组件dom和使用内部闭包变量的情景,这个时候我们就可以使用useRef

useRef返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

function AutoFocusIpt() {
  const inputEl = useRef(null);
  const useEffect(() => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  }, []);
  return (
    <>
      <input ref={inputEl} type="text" />
    </>
  );
}

除了以上应用场景外,我们还可以利用它来实现class组件的setState的功能,具体实现后面会有介绍。

2. 实现一个小型redux

实现redux我们会利用之前说的useReducer, useContext, createContext这三个api,至于如何实现redux,其实网上也有很多实现方式,这里笔者写一个demo供大家参考:

// actionType.js
const actionType = {
  INSREMENT: 'INSREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET'
}
export default actionType

// actions.js
import actionType from './actionType'
const add = (num) => ({
    type: actionType.INSREMENT,
    payload: num
})

const dec = (num) => ({
    type: actionType.DECREMENT,
    payload: num
})

const getList = (data) => ({
    type: actionType.GETLIST,
    payload: data
})
export {
    add,
    dec,
    getList
}

// reducer.js
function init(initialCount) {
  return {
    count: initialCount,
    total: 10,
    user: {},
    article: []
  }
}

function reducer(state, action) {
  switch (action.type) {
    case actionType.INSREMENT:
      return {count: state.count + action.payload};
    case actionType.DECREMENT:
      return {count: state.count - action.payload};
    case actionType.RESET:
      return init(action.payload);
    default:
      throw new Error();
  }
}

export { init, reducer }

// redux.js
import React, { useReducer, useContext, createContext } from 'react'
import { init, reducer } from './reducer'

const Context = createContext()
const Provider = (props) => {
  const [state, dispatch] = useReducer(reducer, props.initialState || 0, init);
    return (
      <Context.Provider value={{state, dispatch}}>
        { props.children }
      </Context.Provider>
    )
}

export { Context, Provider }

其实还有更优雅的方式实现,笔者之前也写了几套redux模版,欢迎一起讨论哈。接下来我们进入正文,来带大家实现几个常用的自定义hooks。

3. 实现自定义的useState,支持类似class组件setState方法

熟悉react的朋友都知道,我们使用class组件更新状态时,setState会支持两个参数,一个是更新后的state或者回调式更新的state,另一个参数是更新后的回调函数,如下面的用法:

this.setState({num: 1}, () => {
    console.log('updated')
})

但是hooks函数的useState第二个参数回调支持类似class组件的setState的第一个参数的用法,并不支持第二个参数回调,但是很多业务场景中我们又希望hooks组件能支持更新后的回调这一方法,那该怎么办呢?其实问题也很简单,我们只要对hooks原理和api非常清楚的话,就可以通过自定义hooks来实现,这里我们借助上面提到的useRef和useEffect配合useState来实现这一功能。

注:react hooks的useState一定要放到函数组件的最顶层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致,因为useState底层采用链表结构实现,有严格的顺序之分。

我们先来看看实现的代码:

import { useEffect, useRef, useState } from 'react'

const useXState = (initState) => {
    const [state, setState] = useState(initState)
    let isUpdate = useRef()
    const setXState = (state, cb) => {
      setState(prev => {
        isUpdate.current = cb
        return typeof state === 'function' ? state(prev) : state
      })
    }
    useEffect(() => {
      if(isUpdate.current) {
        isUpdate.current()
      }
    })

    return [state, setXState]
  }

export default useXState

笔者利用useRef的特性来作为标识区分是挂载还是更新,当执行setXstate时,会传入和setState一模一样的参数,并且将回调赋值给useRef的current属性,这样在更新完成时,我们手动调用current即可实现更新后的回调这一功能,是不是很巧妙呢?

4. 实现自定义的useDebounce

节流函数和防抖函数想必大家也不陌生,为了让我们在开发中更优雅的使用节流和防抖函数,我们往往需要让某个state也具有节流防抖的功能,或者某个函数的调用,为了避免频繁调用,我们往往也会采取节截流防抖这一思想,原生的节流防抖函数可能如一下代码所示:

// 节流
function throttle(func, ms) {
    let previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > ms) {
            func.apply(context, args);
            previous = now;
        }
    }
}

// 防抖
function debounce(func, ms) {
    let timeout;
    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout);

        timeout = setTimeout(() => {
            func.apply(context, args)
        }, ms);
    }
}

那么我们首先来实现一下防抖的hooks,代码如下:

import { useEffect, useRef } from 'react'

const useDebounce = (fn, ms = 30, deps = []) => {
    let timeout = useRef()
    useEffect(() => {
        if (timeout.current) clearTimeout(timeout.current)
        timeout.current = setTimeout(() => {
            fn()
        }, ms)
    }, deps)

    const cancel = () => {
        clearTimeout(timeout.current)
        timeout = null
    }

    return [cancel]
  }

export default useDebounce

由代码可以知道,useDebounce接受三个参数,分别为回调函数,时间间隔以及依赖项数组,它暴露了cancel API,主要是用来控制何时停止防抖函数用的。具体使用如下:

// ...
import { useDebounce } from 'hooks'
const Home = (props) => {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  const [cancel] = useDebounce(() => {
    setB(a)
  }, 2000, [a])

  const changeIpt = (e) => {
    setA(e.target.value)
  }
  return <div>
    <input type="text" onChange={changeIpt} />
    { b } { a }
  </div>
}

以上代码就实现了state的debounce的功能,具体效果如下图所示: 10分钟教你手写8个常用的自定义hooks

5. 实现自定义的useThrottle

同理,我们继续来实现节流的hooks函数。直接上代码:

import { useEffect, useRef, useState } from 'react'

const useThrottle = (fn, ms = 30, deps = []) => {
    let previous = useRef(0)
    let [time, setTime] = useState(ms)
    useEffect(() => {
        let now = Date.now();
        if (now - previous.current > time) {
            fn();
            previous.current = now;
        }
    }, deps)

    const cancel = () => {
        setTime(0)
    }

    return [cancel]
  }

export default useThrottle

代码和自定义useDebounce类似,但需要注意一点就是为了实现cancel功能,我们使用了内部state来处理,通过控制时间间隔来取消节流效果,当然还有很多其他方法可以实现这个hooks API。具体效果如下: 10分钟教你手写8个常用的自定义hooks

6. 实现自定义useTitle

自定义的useTitle hooks其实使用场景也很多,因为我们目前大部分项目都是采用SPA或者混合SPA的方式开发,对于不同的路由我们同样希望想多页应用一样能切换到对应的标题,这样可以让用户更好的知道页面的主题和内容。这个hooks的实现也很简单,我们直接上代码:

import { useEffect } from 'react'

const useTitle = (title) => {
    useEffect(() => {
      document.title = title
    }, [])

    return
  }

export default useTitle

以上代码可以看出我们只需要在useEffect中设置document的title属性就好了,我们不需要return任何值。其实还有更优雅和复杂的实现方法,这里就不一一举例了。具体使用如下:

const Home = () => {
    // ...
    useTitle('趣谈前端')

    return <div>home</div>
}

7. 实现自定义的useUpdate

我们都知道如果想让组件重新渲染,我们不得不更新state,但是有时候业务需要的state是没必要更新的,我们不能仅仅为了让组件会重新渲染而强制让一个state做无意义的更新,所以这个时候我们就可以自定义一个更新的hooks来优雅的实现组件的强制更新,实现代码如下:

import { useState } from 'react'

const useUpdate = () => {
    const [, setFlag] = useState()
    const update = () => {
        setFlag(Date.now())
    }

    return update
  }

export default useUpdate

以上代码可以发现,我们useUpdate钩子返回了一个函数,该函数就是用来强制更新用的。使用方法如下:

const Home = (props) => {
  // ...
  const update = useUpdate()
  return <div>
    {Date.now()}
    <div><button onClick={update}>update</button></div>
  </div>
}

效果如下: 10分钟教你手写8个常用的自定义hooks

8. 实现自定义的useScroll

自定义的useScroll也是高频出现的问题之一,我们往往会监听一个元素滚动位置的变化来决定展现那些内容,这个应用场景在H5游戏开发中应用十分广泛,接下来我们来看看实现代码:

import { useState, useEffect } from 'react'

const useScroll = (scrollRef) => {
  const [pos, setPos] = useState([0,0])

  useEffect(() => {
    function handleScroll(e){
      setPos([scrollRef.current.scrollLeft, scrollRef.current.scrollTop])
    }
    scrollRef.current.addEventListener('scroll', handleScroll, false)
    return () => {
      scrollRef.current.removeEventListener('scroll', handleScroll, false)
    }
  }, [])

  return pos
}

export default useScroll

由以上代码可知,我们在钩子函数里需要传入一个元素的引用,这个我们可以在函数组件中采用ref和useRef来获取到,钩子返回了滚动的x,y值,即滚动的左位移和顶部位移,具体使用如下:

import React, { useRef } from 'react'

 import { useScroll } from 'hooks'
const Home = (props) => {
  const scrollRef = useRef(null)
  const [x, y] = useScroll(scrollRef)

  return <div>
      <div ref={scrollRef}>
        <div className="innerBox"></div>
      </div>
      <div>{ x }, { y }</div>
    </div>
}

通过使用useScroll,钩子将会帮我们自动监听容器滚动条的变化从而实时获取滚动的位置。具体效果如下: 10分钟教你手写8个常用的自定义hooks

9. 实现自定义的useMouse和实现自定义的createBreakpoint

自定义的useMouse和createBreakpoint的实现方法和useScroll类似,都是监听窗口或者dom的事件来自动更新我们需要的值,这里我就不一一实现了,如果不懂的可以和我交流。通过这些自定义钩子能大大提高我们代码的开发效率,并将重复代码进行有效复用,所以大家在工作中可以多尝试。

当我们写了很多自定钩子时,一个好的开发经验就是统一管理和分发这些钩子,笔者建议可以在项目中单独建一个hooks的目录专门存放这些可复用的钩子,方便管理和维护。如下: 10分钟教你手写8个常用的自定义hooks

最后

如果想获取本次项目完整的源码, 或者想学习更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。 10分钟教你手写8个常用的自定义hooks

更多推荐

收藏
评论区

相关推荐

10分钟教你手写8个常用的自定义hooks
前言 Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。本文是一篇以实战为主的文章,主要讲解实际项目中如何使用hooks以及一些最佳实践,不会一步步再介绍一遍react hooks的由来和基本使用,因为写hooks的文章很多,而且官网对于react hooks的介绍也很详细
5分钟简单了解React
![](https://oscimg.oschina.net/oscnet/9f54c355-5b5d-4c33-bd28-d042a50ded80.jpg) 首先附上官网正文😀:\[React Hooks\](\[https://reactjs.org/docs/hooks-intro.html\](https://reactjs.org/docs/ho
10分钟教你手写8个常用的自定义hooks
https://juejin.im/post/5e57d0dfe51d4526ce6147f2 前言 -- Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。本文是一篇**以实战为主的文章,主要讲解实际项目中如何使用hooks以及一些最佳实践**,不会一步步再介
15 个很有用的自定义 React Hooks
React 提供了自定义 Hooks 的特性,我们可以根据这个特性创建出很多有意思的功能,今天就来分享一些个人认为还不错的 Hooks,共计 15 个。 如果对于 React Hooks 还不是特别熟悉的话,可以先参阅这篇文章:[React Hooks 温故而知新](https://www.oschina.net/action/GoToLink?url=h
CentOS7搭建git私库及git使用hooks提交自动发布代码
#环境 CentOS7.3 + git version 1.8.3.1 yum -y install git useradd git passwd git 123 #新建/git/gitdemo目录并初始化 mkdir -p /git/gitdemo cd /git/gitdemo git init --bare #新建并编辑post
Git Web Hooks 的部署
Git Web Hooks 的部署 ================= * \[Web Hooks 的介绍\](#Web Hooks 的介绍) * \[Web Hooks 在Linux中的坑\](#Web Hooks 在Linux中的坑) * \[Web Hooks 在Windows Server中的坑\](###Web Hooks 在Wind
React hooks能替代HOC和render props么?
最近开始学习React,记录一下心得。 React hooks是16.8.0推出的,其目的是为了替换class,HOC,render props。那么本文将讨论啥是hooks,HOC,render props,以及hooks究竟能不能替换掉HOC,render props。 Hooks替代HOC和render props。 ===============
React技术栈实现XX点评电商App
> 项目地址:[https://github.com/Nealyang/React-Fullstack-Dianping-Demo](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fgithub.com%2FNealyang%2FReact-Fullstack-Dianping-Demo)
React系列
React系列-Mixin、HOC、Render Props(上) React系列-轻松学会Hooks(中) React系列-自定义Hooks很简单(下) > 我们在第二篇文章中介绍了一些常用的hooks,接着我们继续来介绍剩下的hooks吧 useReducer ---------- > 作为useState 的替代方案。它接收一个形如`(sta
Redux入门到使用。
#### ![](https://oscimg.oschina.net/oscnet/c7b0cf8db4ed63eccfdfe3b7d711ec72da6.gif)简介 Redux是针对JavaScript应用的可预测状态容器。 > 如果熟悉设计模式之观察者模式理解起来就简单了。就是将你在其他组件中需要用到的数据放到一个容器中,那么组件就可以从其中取放
Redux入门实战——todo
1.前言 ==== 在之前的博客中,我写了一篇关于todo-list实现的博客,一步一步详细的记录了如何使用基础的React知识实现一个React单页面应用,通过该篇文章,能够对React入门开发有一个直观的认识和粗浅的理解。 近期,个人学习了一下Redux,又将该项目使用 React+Redux的方式进行了实现。本片内容记录以下实践的过程。通过本实例,
Redux异步解决方案之Redux
前段时间,我们写了一篇[Redux源码分析的文章](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fjuejin.im%2Fpost%2F6845166891682512909),也[分析了跟`React`连接的库`React-Redux`的源码实现](https://www.oschina
Requests 使用 钩子 实现文件上传到其他服务器
        // 使用钩子来处理有文件的上传.         if($hasFile){           $hooks = new \Requests_Hooks();           $hooks->register('curl.before_send', function($fp, $data=array(
ReviewBoard实现自动post
ReviewBoard实现自动post-commit 在svn hooks中使用RBTools工具的rbt post命令向Reviewboard自动提交review request(评审请求) 在实践中遇到的主要问题为:rbt命令在linux终端可以执行但在svn hooks中执行出错 经过不断定位、尝试,终于解决了该问题,记录并分享... 欢迎交流
taro 知识点
* taro 的包: 包名 说明 @tarojs/redux Redux for Taro @tarojs/redux-h5 Forked react-redux for taro @tarojs/plugin-csso Taro压缩CSS文件 ### 内置环境变量 `process.env.TARO_ENV`用于判断当前编译类型,目