react Hooks 实践总结

文章转自https://caibowen.net/article/5a5ad0692f7d

常用的官方 hooks(官方文档

useState

这个应该是我们最常用的 hook 了,用来代替 class 组件中的 setState 基础用法:const [state, setState] = useState(initialState);

**需要注意的点【查看代码】 **

  • setState 这里接受的参数除了一个给定的值外,还可以接受一个函数。例如

    const [count, setCont] = useState(0);
    
    setCount(count+1)
    setCount((prevCount) => prevCount + 1)
    // 上面用法通常使用在,我没法保证我获取的 count 是最新的值的情况下,比如在 useCallback 中
    
    const fn = useCallback(() => {
      console.log(count); // 这里的count 会一直是初始化的那个count,这个时候如果用 setCount(count + 1) 就会出现问题
      setCount((prevCount) => prevCount + 1); // 这样就不会有问题
    }, []) // 这里最好传递要用到的依赖,否则会被缓存,获取不到最新的值
    
    
  • setState 会对值进行比较,如果 prevValue 跟这次的 value 相等的话(比较只是进行了浅比较),将不会触发更新。如果想强制每次更新,可以自定义一个hooks,例如

    // 如果是class 组件,每次调用setState 都会触发render,它不会对前后的值进行比较
    const useFocusUpdate = () => {
      const [ignore, update] = useState(0);
      return () => {
        update(ignore + 1);
      };
    };
    
    
  • initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。

useEffect

基础用法: useEffect(fn, [deps]) 【后面的Fn, deps,均指的此处,Fn2指的是 Fn的返回函数】 默认情况下useEffect 接受的函数 Fn 会在 render 之后执行,Fn 执行后,如果返回了一个函数Fn2,那么Fn2,在执行下一个 effect 之前,即上一个 effect 销毁时

export default function App() {
  console.log('start')
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('log on effect', count);
    return () => {
      console.log('log on effecct return fn', count);
    }
  }, [count])
  console.log('render')
  return (
    <div className="App">
      <h1>{count}</h1>
      <h2 onClick={() => setCount(count + 1)}>Add Count</h2>
    </div>
  );
}

// 打印顺序
// start render log on effect 0
// 点击 Add Count 触发 count 更新,打印顺序
// start render log on effecct return fn 0 log on effect 1

需要注意的点

  • 在Fn中使用到的变量需要传入的 deps,否则会出现取不到最新的值的问题

  • 传入 deps 的值也需要注意,不要传递函数中的方法,例如

    // 这样会造成死循环,因为每一次重新 render handle 就会被重新创建,useEffect 每次都会检测到变化,每次就会执行
    function App() {
      const [count, setCount] = useState(0);
      const handle = () => { setCount(count + 1) };
      useEffect(() => {
        handle();
      }, [fn])
      return (
        <div className="App">
        </div>
      );
    }
    
    

useEffect 经常被用到的使用场景

  • 模拟 DidMount useEffect(fn, []) 传入一个空数组即可,fn 会在首次 render 后执行,之后再也不会执行,fn 返回的函数会在组件销毁时执行,即为 willUnmount

  • 用来监变量变化,例如

    // 当 city 出现变化后,我们重新开始拉取 城市信息,城市下面的运营信息,城市下面的POI等信息
    useEffect(() => {
      if (cityId !== null) {
        fetchCityInfo();
        fetchCategoryList();
        fetchOperationResource();
        fetchPoiList();
      }
    }, [cityId]);
    
    

useCallback

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

为什么会有这个hook呢?因为前面说的,组件更新的时候,组件内的内联函数都会被重新创建,如果我们将这个内联函数变成依赖,或者传递给某个子组件,都会导致一些性能问题。

应用场景

// 这种写法,如果 App 需要re-render,Children 也会出现 re-render,因为handle这个函数每次都被重新被创建了
function App() {
  const handle = () => { ... }
  return (
    <div className="App">
      <Children onClick={handle}/>
    </div>
  );
}

function App() {
  const handle = useCallback(fn, [deps]); // fn 中如果用到了某个state,需要放在deps中,否则拿到的 state 不是最新的
  return (
    <div className="App">
      <Children onClick={handle}/>
    </div>
  );
}

useMemo

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

应用场景

  • 需要复杂计算的时候,可以用来存储, 例如

    function App() {
      // computeExpensiveValue 是一个非常复杂的计算函数,这样做了后,只要依赖的 a 不发生变化,
      // computeExpensiveValue是不会重复执行的,value是一个 memoized 值。
      const value = useMemo(() => computeExpensiveValue(a), [a]);
      return (
        <div className="App">
          {value}
        </div>
      );
    }
    
    
  • 用 useMemo 来优化引用数据的传递,例如

    // 这种写法,如果 App 需要re-render,Children 也会出现 re-render,因为 Children 的 style 这个值一直都被重新创建
    function App({ height }) {
      return (
        <div className="App">
          <Children style={{ height }}/>
        </div>
      );
    }
    
    // 用 useMemo 来记忆,减少 Children 的 re-render
    function App({ height }) {
      const childrenStyle = useMemo(() => ({ height }), [height])
      return (
        <div className="App">
          <Children style={childrenStyle}/>
        </div>
      );
    }
    
    

useRef

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

基本用法跟注意点【查看代码

  • 用来访问dom节点,或者访问一堆列表的节点

    // 注意 需要首次渲染结束后,ref.current 上面才有对应的 dom。如果 dom 被移除了对应的 ref.currnt 就会变成 null
    export default function App() {
      const [show, setVisible] = useState(true);
      const ref = useRef(null);
      const listRef = useRef([]);
      useEffect(() => {
        console.log(listRef);
      }, [ref2])
    
      return (
        <div className="App">
          {show && <div ref={ref}>dom1</div>}
          {ary.map((num, index) => <div key={num} ref={(_ref) => {
            listRef.current[index] = _ref;
          }}>{num}</div>)}
          <div onClick={() => {
            console.log(ref.current);
            setVisible(!show)
          }}>click me</div>
        </div>
      );
    }
    
    
  • ref是不能被监听的

    export default function App() {
      const ref2 = useRef(0);
      useEffect(() => {
        // ref2 改变后 并不会重新触发此函数
        console.log(ref2);
      }, [ref2])
      return (
        <div className="App">
          <div onClick={() => {
            ref2.current = Math.random();
          }}> change ref</div>
        </div>
      );
    }
    
    
  • 用来存储一些不用引起 re-render 但是需要变化的变量。例如

    export default function App() {
      const hadSendMc = useRef(false);
    
      return (
        <div className="App">
          <div onClick={() => {
            if(hadSendMc.current){
              return;
            }
            console.log('send mc')
            hadSendMc.current = true;
          }}>send mc</div>
        </div>
    
    

useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。 我在项目中只用到了一次,主要是给父组件暴露自定义的方法,而不是一股脑的直接返回ref,房子父组件操作ref导致问题

使用示例

const Video = ({ ... }: PropTypes, ref: any) => {
  const videoRef: any = useRef(null);
  const playVideo = () => {...};
  const handlePlayBtnClick = () => {...};

  useImperativeHandle(ref, () => ({
    play: playVideo,
    pause: () => {
      if (videoRef.current) {
        videoRef.current.pause();
        setVideoVisible(false);
      }
    },
  }));

  return (
    <div className={styles.container}>
      <video ref={videoRef}>
        <source src={videoUrl} type="video/mp4" />
      </video>
    </div>
  );
};

export default React.memo(React.forwardRef(Video));

后续如果使用了,我会同步补上 useContext (这个我没有用过,略) useReducer (这个我没有用过,略) useLayoutEffect (这个我没有用过,略) useDebugValue (这个我没有用过,略)

常用的优化手段

  • 使用 React.memo 去优化 由于 Function Component 组件没有类似 shouldComponentUpdate 这样的方法,也没法继承 PureComponent,我们就可以使用 React.memo去做优化 注意: React.memo仅影响 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。 使用示例:React.memo(MyComponent, areEqual); 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

  • 利用 useCallback 跟 useMemo 去缓存数据,具体可以看上面的示例

  • 如果使用了 react-redux,可以通过 useSelect 的第二个参数去做优化 使用示例:const result : any = useSelector(selector : Function, areEqual? : Function)

项目中自定义的 hook 梳理

针对具体的业务具体开发,笔者这里只是抛砖迎玉

  • 发送页面PV

    export const usePageView = (pageId) => {
      const hadSendPv = useRef(false);
    
      useEffect(() => {
        if (hadSendPv.current) {
          return;
        }
        sendPv();
      }, [pageId]);
    };
    
    
  • useIntersectionObserver 用来监听 dom 是否出现在可视窗口。注意需要引用 intersection-observer-polyfill

    export const useIntersectionObserver = (cb: ObserveCallback, ref?: any, startObserver = true) => {
      const io: any = useMemo(() => new IntersectionObserver((IntersectionObserverEntryList: any) => cb(IntersectionObserverEntryList, io)), [cb]);
      const observerRef: any = useRef(ref || null);
    
      useEffect(() => {
        if (!observerRef.current || !startObserver) {
          return;
        }
        if (observerRef.current instanceof Array) {
          const eleList = observerRef.current;
          eleList.forEach((ele: any) => {
            io.observe(ele);
          });
          return () => {
            eleList.forEach((ele: any) => {
              io.unobserve(ele);
            });
          };
        }
        const ele = observerRef.current;
        io.observe(ele);
    
        return () => {
          io.unobserve(ele);
        };
      }, [io, startObserver]);
      return observerRef;
    };
    
    // 使用方法
    function App({ height }) {
      const observerHandle = useCallback(() => { ... }, []);
      const observerRef = useIntersectionObserver(observerHandle)
      return (
        <div className="App" ref={observerRef}>
          <Children style={{ height }}/>
        </div>
      );
    }
    
    
  • 抽离微信分享设置

    const environment = { ... };
    export const useWechatShare = (shareInfo: any) => {
      if ((environment.isMini() || environment.isWx) && shareInfo.shareUrl) {
        // 微信分享设置
      }
      }, [shareInfo, environment]);
    };
    
    
  • useScroll 页面滚动函数绑定 注意:此处不是相对于body滚动的!

    export const useScroll = (handle: ScrollHandle, threshHold: number = 250) => {
      const handleWrapper = useCallback(throttle(handle, threshHold), [handle]);
      const containerId = 'containerId';
    
      useEffect(() => {
        let listerEle: any = window;
        if (containerId) {
          listerEle = document.getElementById(containerId);
        }
        console.log('useScroll 注入成功');
        if (listerEle) {
          listerEle.addEventListener('scroll', handleWrapper);
          return () => {
            listerEle.removeEventListener('scroll', handleWrapper);
          };
        }
      }, [handleWrapper, containerId]);
     }
    
    

总结

  1. react hooks 的出现提供了我们更好的模块化代码的方式,我们从以前的抽离公共UI的方式,可以变为更小粒度的拆分,例如拆分某个公共的逻辑
  2. react hooks 能够大大的减少我们大代码量,不需要再去理解复杂的class Component,一切都变的更加简单了
  3. 在使用的过程中一定要注意到,Function Component 始终是函数,只要re-render,里面的代码都会重新执行,内联函数,内联对象都会被重新创建

最后再立个 flag

用 react hooks 模拟 class 的各个生命周期

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • React Hooks 在了解React Hooks之前, 我们先看一下 Class 和函数式 的一般写法 Cla...
    YM雨蒙阅读 2,864评论 0 1
  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他...
    mora__阅读 571评论 0 0
  • 1.useState 使用单个 state 变量还是多个 state 变量useState 的出现,让我们可以使用...
    MelousJ阅读 547评论 0 0
  • useState 1.基本使用 等价于 2. 复杂的state 3.使用状态 4. 注意事项 1). 如果stat...
    sweetBoy_9126阅读 3,006评论 0 6
  • 使用React Hooks有什么优势? 什么是hookshook 是一些可以让你在函数组件里面钩入react st...
    Lyan_2ab3阅读 346评论 0 1