React Hooks使用心得!真香

简介: React Hooks使用心得!真香

image.png

前言

分享一下用了将近一年hooks使用心得

动机(官方)

  • 组件之间很难重用有状态逻辑
  • 复杂的组件变得难以理解
  • 类 class 混淆了人和机器
  • 更符合 FP 的理解, React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数image.png

常用 hook

useState
   const [state, setState] = useState(initialState)
  • useState 有一个参数,该参数可以为任意数据类型,一般用作默认值
  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个 setFn。完整例子
function Love() {
    const [like, setLike] = useState(false)
    const likeFn = () => (newLike) => setLike(newLike)
    return (
      <>
        你喜欢我吗: {like ? 'yes' : 'no'}
        <button onClick={likeFn(true)}>喜欢</button>
        <button onClick={likeFn(false)}>不喜欢</button>
      </>
    )
  }

关于使用规则:

  • 只在 React 函数中调用 Hook;
  • 不要在循环、条件或嵌套函数中调用 Hook。让我们来看看规则 2 为什么会有这个现象, 先看看 hook 的组成
function mountWorkInProgressHook() {
 // 注意,单个 hook 是以对象的形式存在的
 var hook = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null
 };
 if (workInProgressHook === null) {
        firstWorkInProgressHook = workInProgressHook = hook;
        /* 等价
            let workInProgressHook = hooks
            firstWorkInProgressHook = workInProgressHook
        */
 } else {
  workInProgressHook = workInProgressHook.next = hook;
 }
 // 返回当前的 hook
 return workInProgressHook;
}

每个 hook 都会有一个 next 指针,hook 对象之间以单向链表的形式相互串联, 同时也能发现 useState 底层依然是 useReducer 再看看更新阶段发生了什么

// ReactFiberHooks.js
const HooksDispatcherOnUpdate: Dispatcher = {
      // ...
     useState: updateState,
  }
  function updateState(initialState) {
    return updateReducer(basicStateReducer, initialState);
  }
function updateReducer(reducer, initialArg, init) {
    const hook = updateWorkInProgressHook();
    const queue = hook.queue;
    if (numberOfReRenders > 0) {
        const dispatch = queue.dispatch;
        if (renderPhaseUpdates !== null) {
            // 获取Hook对象上的 queue,内部存有本次更新的一系列数据
            const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
            if (firstRenderPhaseUpdate !== undefined) {
                renderPhaseUpdates.delete(queue);
                let newState = hook.memoizedState;
                let update = firstRenderPhaseUpdate;
                // 获取更新后的state
                do {
                    // useState 第一个参数会被转成 useReducer
                    const action = update.action;
                    newState = reducer(newState, action);
                    //按照当前链表位置更新数据
                    update = update.next;
                } while (update !== null);
                hook.memoizedState = newState;
                // 返回新的 state 以及 dispatch
                return [newState, dispatch];
            }
        }
    }
    // ...
}

结合实际让我们看下面一组 hooks

let isMounted = false
    if(!isMounted) {
        [name, setName] = useState("张三");
        [age] = useState("25");
        isMounted = true
    }
    [sex, setSex] = useState("男");
    return (
        <button
            onClick={() => {
            setName(李四");
            }}
        >
            修改姓名
        </button>
  );

首次渲染时 hook 顺序为

name => age => sex

二次渲染的时根据上面的例子,调用的 hook 的只有一个

setSex

所以总结一下初始化阶段构建链表,更新阶段按照顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染当两次顺序不一样的时候就会造成渲染上的差异。

为了避免出现上面这种情况我们可以安装 eslint-plugin-react-hooks

// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

useEffect

useEffect(effect, array)
  • effect 每次完成渲染之后触发, 配合 array 去模拟类的生命周期
  • 如果不传,则每次 componentDidUpdate 时都会先触发 returnFunction(如果存在),再触发 effect [] 模拟 componentDidMount [id] 仅在 id 的值发生变化以后触发 清除 effect
useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});

useLayoutEffect

跟 useEffect 使用差不多,通过同步执行状态更新可解决一些特性场景下的页面闪烁问题 useLayoutEffect 会阻塞渲染,请谨慎使用

import React, { useLayoutEffect, useEffect, useState } from 'react';
import './App.css'
function App() {
    const [value, setValue] = useState(0);
    useEffect(() => {
        if (value === 0) {
            setValue(10 + Math.random() * 200);
        }
      }, [value]);
    const test = () => {
        setValue(0)
    }
    const color = !value  ? 'red' : 'yellow'
 return (
  <React.Fragment>
            <p style={{ background: color}}>value: {value}</p>
   <button onClick={test}>点我</button>
  </React.Fragment>
 );
}
export default App;

useContext

const context = useContext(Context)

useContext 从名字上就可以看出,它是以 Hook 的方式使用 React Context, 先简单介绍 Context 的概念和使用方式

import React, { useContext, useState, useEffect } from "react";
const ThemeContext = React.createContext(null);
const Button = () => {
  const { color, setColor } = React.useContext(ThemeContext);
  useEffect(() => {
    console.info("Context changed:", color);
  }, [color]);
  const handleClick = () => {
    console.info("handleClick");
    setColor(color === "blue" ? "red" : "blue");
  };
  return (
    <button
      type="button"
      onClick={handleClick}
      style={{ backgroundColor: color, color: "white" }}
    >
      toggle color in Child
    </button>
  );
};
// app.js
const App = () => {
  const [color, setColor] = useState("blue");
  return (
    <ThemeContext.Provider value={{ color, setColor }}>
      <h3>
        Color in Parent: <span style={{ color: color }}>{color}</span>
      </h3>
      <Button />
    </ThemeContext.Provider>
  );
};

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init)

语法糖跟 redux 差不多,放个基础 🌰

function init(initialCount) {
    return {count: initialCount};
}
function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        case 'reset':
            return init(action.payload);
        default:
            throw new Error();
    }
}
function Counter({initialCount}) {
    const [state, dispatch] = useReducer(reducer, initialCount, init);
    return (
        <>
        Count: {state.count}
<button
    onClick={() => dispatch({type: 'reset', payload: initialCount})}>
    Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}

useRef

const refContainer = useRef(initialValue);

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

  • 解决引用问题--useRef 会在每次渲染时返回同一个 ref 对象
  • 解决一些 this 指向问题
  • 对比 createRef -- 在初始化阶段两个是没区别的,但是在更新阶段两者是有区别的。
  • 我们知道,在一个局部函数中,函数每一次 update,都会在把函数的变量重新生成一次。
  • 所以我们每更新一次组件, 就重新创建一次 ref, 这个时候继续使用 createRef 显然不合适,所以官方推出 useRef。
  • useRef 创建的 ref 仿佛就像在函数外部定义的一个全局变量,不会随着组件的更新而重新创建。但组件销毁,它也会消失,不用手动进行销毁
  • 总结下就是 ceateRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

一个常用来做性能优化的 hook,看个 🌰

const MemoDemo = ({ count, color }) => {
   useEffect(() => {
       console.log('count effect')
   }, [count])
   const newCount = useMemo(() => {
       console.log('count 触发了')
       return Math.round(count)
   }, [count])
   const newColor = useMemo(() => {
       console.log('color 触发了')
       return color
   }, [color])
   return <div>
       <p>{count}</p>
       <p>{newCount}</p>
   {newColor}</div>
}

我们这个时候将传入的 count 值改变 的,log 执行循序

count 触发了
count effect
  • 可以看出有点类似 effect, 监听 a、b 的值根据值是否变化来决定是否更新 UI
  • memo 是在 DOM 更新前触发的,就像官方所说的,类比生命周期就是 shouldComponentUpdate
  • 对比 React.Memo 默认是是基于 props 的浅对比,也可以开启第二个参数进行深对比。在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。使用 useMemo 可以精细化控制,进行局部 Pure

useCallback

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

useCallback 的用法和上面 useMemo 差不多,是专门用来缓存函数的 hooks

下面的情况可以保证组件重新渲染得到的方法都是同一个对象,避免在传给onClick的时候每次都传不同的函数引用

import React, { useState, useCallback } from 'react'
function MemoCount() {
   const [value, setValue] = useState(0)
   memoSetCount = useCallback(()=>{
       setValue(value + 1)
   },[])
   return (
       <div>
           <button
               onClick={memoSetCount}
               >
               Update Count
           </button>
           <div>{value}</div>
       </div>
   )
}
export default MemoCount

自定义 hooks

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook 一般我将 hooks 分为这几类

util

顾名思义工具类,比如 useDebounce、useInterval、useWindowSize 等等。例如下面 useWindowSize

import { useEffect, useState } from 'react';
export default function useWindowSize(el) {
   const [windowSize, setWindowSize] = useState({
       width: undefined,
       height: undefined,
   });
   useEffect(
       () => {
           function handleResize() {
               setWindowSize({
                   width: window.innerWidth,
                   height: window.innerHeight,
               });
           }
           window.addEventListener('resize', handleResize);
           handleResize();
           return () => window.removeEventListener('resize', handleResize);
       },
       [el],
   );
   return windowSize;
}

API

像之前的我们有一个公用的城市列表接口,在用 redux 的时候可以放在全局公用,不用的话我们就可能需要复制粘贴了。有了 hooks 以后我们只需要 use 一下就可以在其他地方复用了

import { useState, useEffect } from 'react';
import { getCityList } from '@/services/static';
const useCityList = (params) => {
   const [cityList, setList] = useState([]);
   const [loading, setLoading] = useState(true)
   const getList = async () => {
       const { success, data } = await getCityList(params);
       if (success) setList(data);
       setLoading(false)
   };
   useEffect(
       () => {getList();},
       [],
   );
   return {
       cityList,
       loading
   };
};
export default useCityList;
// bjs
function App() {
   // ...
   const { cityList, loading } = useCityList()
   // ...
}

logic

逻辑类,比如我们有一个点击用户头像关注用户或者取消关注的逻辑,可能在评论列表、用户列表都会用到,我们可以这样做

import { useState, useEffect } from 'react';
import { followUser } from '@/services/user';
const useFollow = ({ accountId, isFollowing }) => {
    const [isFollow, setFollow] = useState(false);
    const [operationLoading, setLoading] = useState(false)
    const toggleSection = async () => {
        setLoading(true)
        const { success } = await followUser({ accountId });
        if (success) {
            setFollow(!isFollow);
        }
        setLoading(false)
    };
    useEffect(
        () => {
            setFollow(isFollowing);
        },
        [isFollowing],
    );
    return {
        isFollow,
        toggleSection,
        operationLoading
    };
};
export default useFollow;

只需暴露三个参数就能满足大部分场景

UI

还有一些和 UI 一起绑定的 hook, 但是这里有点争议要不要和 ui 一起混用。就我个人而言一起用确实帮我解决了部分复用问题,我还是分享出来。

import React, { useState } from 'react';
import { Modal } from 'antd';
// TODO 为了兼容一个页面有多个 modal, 目前想法通过唯一 key 区分,后续优化
export default function useModal(key = 'open') {
    const [opens, setOpen] = useState({
        [key]: false,
    });
    const onCancel = () => {
        setOpen({ [key]: false });
    };
    const showModal = (type = key) => {
        setOpen({ [type]: true });
    };
    const MyModal = (props) => {
        return <Modal key={key} visible={opens[key]} onCancel={onCancel} {...props} />;
    };
    return {
        showModal,
        MyModal,
    };
}
// 使用
function App() {
    const { showModal, MyModal } = useModal();
    return <>
          <button onClick={showModal}>展开</button>
          <MyModal onOk={console.log} />
       </>
}

总结

越来越多的 react 配套的三方库都上了 hooks 版,像 react-router、redux 都出了 hooks。

同时也出现了一些好用的 hooks 库,比如 ahooks 这种。自从用了 hooks 以后我就两个字,真香

相关文章
|
2月前
|
前端开发 JavaScript 开发者
深入理解React Hooks:提升前端开发效率的关键
【10月更文挑战第5天】深入理解React Hooks:提升前端开发效率的关键
|
2月前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
77 4
|
2月前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
2月前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
26天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
28天前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
22 3
|
1月前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
35 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
37 6
|
22天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
21 0