让咱们坐上 Hooks 的托马斯小火车

在 React 16 中,除去 Fiber 架构外,Hooks 是最使人激动的一个特性,相比于 class component,Hooks 加持后的 function component 在写法与思路上都大有不一样,不少时候显得更为简洁与清爽(熵更低,弱化生命周期的概念),同时解决了使人烦恼的 this 指针指向问题,仍是很香的。react

但理性来讲,到目前为止,hooks 仍是一个坑不少的阶段,而且也缺少一个成体系的最佳实践,如下谈谈我对 hooks 的一些浅薄的认识。git

那么,是时候发车了。github

经常使用Hooks

useState

咱们能够把这里的 state 看作咱们在 class component 中使用的 this.state。ajax

咱们的每一次 setState 操做,在改变了值以后,都会引起 rerender 操做,从而触发页面的更新(可是若是没有改变的话,则不会触发 rerender,咱们在后面将会利用这一特性作一件有趣的事情)。编程

同时,setState 能够以函数做为参数,这个时候咱们能够获取到最新的 state 值(在第一个回调参数)。redux

import React, { useState } from 'react';

function App() {
    const [ state, setState ] = useState(0);
    return (
        <span>{state}</span>
    )
}
复制代码

useEffect

useEffect能够说是全部 hooks API 中最像是声明周期的钩子了,很容易让人理解成为,若是依赖数组为空,那么它等价为 componentDidMount,可是真的这样吗?数组

咱们能够这样去理解咱们的函数组件,函数组件的每次运行都至关于 class component 中的一次 render,每轮都会保留它的闭包,因此,咱们的 useEffect 实际保留了它运行轮次的 state 和 props 状态(若是依赖不更新,那么状态不更新),这也就是 useEffect 和 componentDidMount 生命周期的关系。缓存

import React, { useEffect } from 'react';
function App() {
    useEffect(() => {
        console.log('I am mount');
        return () => {
            console.log('before next run, I am cleaned');
        }
    }, []);
复制代码

useLayoutEffect

useLayoutEffect 与 useEffect 的不一样在于,useLayoutEffect 会在 DOM 渲染以前执行,而 useEffect 会在 DOM 渲染以后执行,因此咱们能够利用这个特性,避免一些因为 DOM 渲染以后进行操做致使的白屏问题。性能优化

useCallback

useCallback 能够帮助咱们缓存函数(useMemo一样能够作到,写法不一样),经过手动控制依赖,作到减小由于函数的更新致使子组件的更新(带来的性能问题很是明显)antd

import React, { useCallback } from 'react';
function App() {
    const cb = useCallback(() => { console.log('callback') }, []);
    return (
        <button onClick={cb}></button>
    )
}
复制代码

useMemo

useMemo 能够为咱们的 function component 提供缓存的能力,在一些重计算的场景下,能够减小重复计算的次数,起到明显的性能提高。 固然,useMemo一样能够用来缓存组件,起到相似与 class component 中 shouldComponentUpdate 的做用,让咱们手动经过管理依赖的方式作到控制子组件的更新(固然这个手动管理的成本是很是高的)

useRef

由于在 hooks 中,咱们所声明的全部变量是只属于它的闭包的,因此,咱们没法作到变量的一个共享。由于 immutable 的 state 并不适合咱们存储一些不参与 UI 显示的变量。hooks 为咱们提供了 useRef 去存储 mutable 的不参与 UI 显示的变量,而且能够在每一轮 render 中共享。

useRef 不只能够用来存储对 Dom 元素的引用(它的本意),更能够用来存储咱们须要在每轮 render 中共享的 mutable 的变量(可能很是经常使用)。

import React, { useRef } from 'react';
function App() {
  const td = useRef(1);
  console.log(td.current); // 1
  ...
复制代码

useReducer

在当前版本中的 useReducer 事实上是对 useState 的一层封装,实现了 redux 的一套原理(以前的版本是 useState 是对 useReducer 的一层封装)

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}
复制代码

useContext

假定咱们已经有了一个 Context ,而且咱们的子组件已经在 Provider 包裹下,咱们能够直接使用 useContext 去获取值,而非使用回调去获取值。 同时,咱们也能够对某些 Context 进行 useContext 的封装,让咱们能够在不一样的组件中方便的使用 Context 中的数据。

// 假定咱们已经有 Context
function Child(props) {
    const { value } = useContext(Context);
    return (
      <div> {value} </div>
    )
}
复制代码

咱们能够将 Context 、useReducer 与 useContext 结合起来,打造咱们本身的 Redux

const CTX = React.createContext(null);
const reducer = (state, action) => {
    switch(action.type) {
        default:
            reutrn state;
    }
}
const Context = function({ children }) {
    const [state, dispatch] = useReducer(reducer, {});
    return (
        <CTX.Provider value={ state, dispatch }> {children} </CTX.Provider> ) } 复制代码

怎么理解Hooks中的状态

咱们应该树立一个理念,在 function component 中,全部的状态,都是隶属于它的闭包的,因此致使了 咱们每一轮的 Render 都会有本身的一个闭包,全部的 useEffect 与 useLayoutEffect 都在其最后一次更新的闭包中 Hooks处理请求

正确处理依赖

hooks 编程有些相似于响应式编程,同时,为了能够老是拿到最正确的值,正确的去书写 hooks依赖 是很是重要的,也就是所谓的对依赖诚实,这样才能保证咱们最终发送请求之时,能够取到正确的 state 和 props。

放置依赖的请求于 useEffect 中

为了可以正确的处理请求,有一种想法是——将请求的函数放置于 useEffect 中,这样子就能够确保咱们每时每刻都会去正确的处理其中的依赖问题。 处理竞态 咱们知道,在 hooks 里,每一次 Render 以及 每一次 useEffect 的执行都是在它本身所处轮次的闭包中,因此,咱们处理竞态的一个思路就来源于这里。

咱们的依赖变化会触发咱们的 ajax 操做,因此当第二次请求发生时,实际上上一次 effect 已经到了清理反作用时期,因此执行了 return 中的函数,将咱们的flag置为true,这样,当咱们的请求返回之时,其effect 所在的闭包是能够感知到执行结束的状态的,从而抛弃旧值,达到对竞态的正确处理。

useEffect(() => {
    let flag = false;
    ajax().then(res => {
        if (!flag) {
            //...do something
        }
    })
    return () => {
        flag = true;
    }
}, [deps])
复制代码

请求与触发分离

请求

咱们能够将请求函数用普通函数的方法,放置于整个 function 中,这样足以确保咱们这个函数可以拿到当前 render 轮次所依赖的 state 和 props,若是有性能方面的顾虑,能够考虑使用 useCallback 去进行包装(但此时必定要对依赖诚实)

function App() {
    const [flag, setFlag] = useState(0);

    const ajax = () => {
       _ajax(props)
    };

    useEffect(() => {
        ajax();
    }, [flag]);

    return (
        ...
    )
}
复制代码

触发

这个时候必定要注意的一点是,咱们的触发 flag,必定要在最后修改(先进行预操做——其它的 state 修改),肯定咱们的 effect 更新时,索引用的,是最新的 ajax 请求函数。

非渲染参数使用 ref 进行保存 由于咱们在 effect 中,永远能够正确的获取到 ref 值,因此,当咱们的参数不参与渲染时,咱们能够用 useRef 生成的 ref 对其进行管理,这样咱们就能够不用去担忧因为 ref 所引用参数的变化问题(同时,也不会触发页面的 rerender)

const name = useRef('小明')
const ajax = useCallback(() => {
    ajax({ name })
}, []);

// 修改 param 直接操做 ref
name.current = '123';
复制代码

极限性能Trick

减小计算

依赖数组欺骗

利用 setState 的回调处理获取 state 的问题 由于

const [state, setState] = useState(0);

// 利用 setState 的回调拿到最新的 state,返回原值,能够不触发 rerender(极端状况下能够用于性能优化)
const update = useCallback(() => {
    setState(state => {
        // 作你想作的任何事情
        return state;
    })
}, []);
复制代码

试想一个很骚的场景,若是咱们使用 setter 嵌套(而且都返回原始值),那么咱们是否是能够在无任何依赖状况下用 state 作任何想作的事情呢(代码可读性忽略)

const trigger = useCallback(() => {
    setState1(state1 => {
        setState2(state2 => {
            console.log(state1 + state2);
            return state2;
        })
        return state1;
    })
});
复制代码

利用 useReducer 和 setState 结合处理获取 state 和 props 的问题 由于上面的方法,咱们只能确保咱们能够无依赖的拿到 state ,可是咱们却不能在无依赖的状况下拿到 props 那么咱们能够怎么办呢。 咱们可能把 useReducer 的 reducer 放在 function component 函数体内,利用 dispatch 最终触发的是最新的闭包中的 reducer 来确保咱们能够拿处处于最新状态的 props

function App({ a, b, c }) {
    const reducer = (state, action) => {
        switch(action.type) {
            case 'init':
                // 这里永远能够拿到最新的 a
                return Object.assign(state, { a: a });
            default:
                return state;
        }
    }
    const [state, dispatch] = useReducer(reducer, {});
    return (
        <div>{ state.a }</div>
    )
}
复制代码

减小Render

咱们能够作相似于 class component 中的 PureComponent 这样的操做,咱们能够用 React.memo 包裹大部分的组件(会带来额外的比较,性能不必定是最佳的)

直接使用 React.memo

利用 React.memo,咱们能够作到让 React 对咱们的组件进行浅比较,

const Child = function({ a, b, c }) {
    return <div>{a}{b}{c}</div>
} 
export default React.memo(Child);
复制代码

使用 useMemo 进行细粒度的控制

function App({ a, b, c }) {
    const RenderComponent = useMemo(() => {
        return <div>{c}</div>
    }, [c]);
    return (
        <RenderComponent /> ) } 复制代码

使用 useCallback 包裹函数,使函数变化减小

在这里,可使用我上面所介绍的 trick ,减小依赖的数量,从而减小 rerender 的次数 Eg: trigger.jsx 开关

const useTrigger = () => {
    const [state, setState] = useState(false);
    const trigger = useCallback(() => {
        setState(ste => !ste);
    }, []);
    return { state, trigger };
}

// vs

const useTrigger = () => {
    const [state, setState] = useState(false);
    const trigger = useCallback(() => {
        setState(ste => !ste);
    }, [state]);
    return { state, trigger };
}
复制代码

Hooks 实践

谈谈表单

像双向数据绑定那样编写表单

const useInput = () => {
  const [value, setValue] = useState('');
  const onChange = val => {
    setValue(val.target.value);
  };
  return {
    value,
    onChange
  };
};
复制代码

表单提交

export const useSubmit = submitFunction => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [res, setRes] = useState(null);

  const trigger = useCallback(() => {
    try {
      if (_.isFunction(submitFunction)) {
        (async () => {
          let res = await submitFunction();
          if (res) {
            setRes(res);
          }
        })();
      }
    } catch (e) {
      setError(e);
    } finally {
      setLoading(true);
    }
  }, [submitFunction]);

  return [loading, res, error];
};
复制代码

利用 useMemo 操做 props 数据

不少时候,咱们都会依赖于 props 去计算咱们的 state,在 class component 中给咱们提供了 getDerivedStateFromProps 生命周期供咱们去作相似的操做,可是在 hooks 里,咱们并无这样的生命周期的概念,那咱们应该如何去作呢?

咱们能够利用 useMemo 去进行对 props 的计算操做,经过正确处理依赖,就能够籍由 useMemo 的记忆特性,让咱们以最小的成本去正确的更新 state (高成本的方案是每一次去计算将值赋给闭包中的普通变量)。

import React, { useMemo } from 'react';

function App({ data }) {
    // 只有 data 更新时从新计算
    const info = useMemo(() => {
        // 对 data 进行一系列的计算操做
        return newData;
    }, [data]);
}
复制代码

利用 hooks 返回组件

以前所说的大都是利用 hooks 去处理逻辑问题,那么 hooks 是否能够像是高阶组件那样,为咱们返回一个组件呢,答案是能够的,而且利用这样的能力,咱们还能够简化不少状况下咱们的编程。

import React, { useState, useCallback } from 'react';
import { Modal } from 'antd';

export default function useModal() {
  const [show, setShow] = useState<boolean>(false);

  const openModal = useCallback(() => {
    setShow(true);
  }, []);

  const closeModal = useCallback(() => {
    setShow(false);
  }, []);

  const CusModal: React.SFC = ({ children, ...props }) => {
    return (
      <Modal visible={show} {...props}> {children} </Modal>
    )
  }

  return {
    show,
    setShow,
    openModal,
    closeModal,
    CusModal
  }
}
复制代码

利用 ref hooks 进行一些无侵入操做(react 官方 不推荐) 由于 ref 能够拿到原始 dom,咱们能够利用这个特性作一些操做,例如说侵入代码性的埋点迁移至 ref(减小对原始代码侵入)

eg:利用 ref 记录停留时间(能够作无侵入埋点)

export const useHoverTime = eventName => {
  const EV = `${ eventName}`;
  const ref = useRef(null);

  useEffect(() => {
    localStorage.setItem(EV, 0);
    return () => {
      const time = localStorage.getItem(EV);
      // do something 
      localStorage.setItem(EV, null);
    };
  }, []);

  useEffect(() => {
    let startTime = null;
    let endTime = null;
    const overHandler = () => {
      startTime = new Date();
    };
    const outHandler = () => {
      endTime = new Date();
      localStorage.setItem(
        EV,
        parseInt(localStorage.getItem(EV)) +
        parseInt(endTime - startTime)
      );
      startTime = 0;
      endTime = 0;
    };
    if (ref.current) {
      ref.current.addEventListener('mouseover', overHandler);
      ref.current.addEventListener('mouseout', outHandler);
    }
    return () => {
      if (ref.current) {
        ref.current.removeEventListener('mouseover', overHandler);
        ref.current.removeEventListener('mouseout', outHandler);
      }
    };
  }, [ref]);
  return ref;
};
复制代码

React-hook-form 利用 ref 进行的表单的注册和提交拦截(我的认为也是一种很是清奇的思路)

Hooks with Immer.js

immutable.js 的使用复杂度是很是高的,可是有时候咱们又但愿咱们的 React App 性能更好,节省没必要要的 rerender,那么 Immer.js 就是一个很是好的选择(事实上dva也使用了immer做为底层库)

咱们能够在使用 useReducer 的时候,使用 Immer 进行状态的变动,从而使得咱们最新的 state 是 immutable 的。

const reducer = (state, action) => {
  switch (action.type) {
    case 'initData':
      return produce(state, draft => {
        draft.data = action.data;
      });
复制代码

让 useReducer 用上 Redux 中间件生态

从何种角度看,useReducer + useContext + Context 的组合都在作传统 Redux 所在作的事情,那么,有没有可能让咱们的原生 hooks 使用上 Redux 的中间件呢(本质上劫持了 action ,与 Redux 的 Api 无关)?! 是能够的,事实上,这里至关于把 Redux 中间件的实现迁移到了 hooks 上,咱们固然能够本身实现,可是 react-use 这个库里帮咱们作了集成,咱们能够方便的直接使用它。

// 建立加强了中间件的 reducer , 这里的例子增长了 redux-logger 与 redux-thunk
const useLoggerReducer = createReducer(logger, thunk);

export default function App() {
  const [state, dispatch] = useLoggerReducer(reducer, initState);
复制代码

这样子,咱们即可以利用 redux-thunk、redux-saga 等中间件进行异步任务的处理,使用 redux-logger 进行 action 的打印和先后 state 的 diff。

打造本身的 combindReducer (代码思路来源于 Middle)

const combineReducers = (reducers) => {
    const keys = Object.keys(reducers);
    const initObj = {};
    keys.forEach(key => {
        let draftState = reducers[key](undefined, { type: '' });
        if (!draftState) {
            draftState = {};
            console.warn(
                `[Error]: 在combineReducers 中 Reducer 须要初始化!`
            );
        }
        initObj[key] = draftState;
    })
    return (state, action) => {
        keys.forEach(key => {
            const prevState = initObj[key];
            initObj[key] = reducers[key](prevState, action);
        });
        return { ...initObj };
    }
}
复制代码

将它和咱们的加强后的 useReducer 结合起来,咱们便拥有了一个几乎能够媲美 redux 的 reducer。

Hooks 第三方工具集合 react-use

多是目前社区中得到 star 和关注最多的自定义 hooks 项目,提供了很是多的自定义 hooks(不少是香的) react-use

Hooks 请求工具 swr

在 React hooks 雨后春笋般的请求库中,最为亮眼的当属于 swr。详情请见官方 github 仓库 swr

Hooks 第三方表单库 react-hook-form

一个很好用的 react-hook-form 表单库。详情请见官方 github 仓库 react-hook-form

相关文章
相关标签/搜索