一次搞定七大 React Hooks

本文首发于个人博客

上次的一次搞定前端四大手写在知乎上收获了500多个赞,简直让我受宠若惊。今天就趁热打铁,写一下一次搞定七大 React Hooks,一方面是为了复习下 React,另一方面是跟小伙伴们分享一些我学习 React Hooks 时的心得体会。由于水平有限,目前只能从 React Hooks 的基本使用方法使用要点上做些分享,关于 Hooks 的原理上的探究日后再做更新。

让我们先来看道字节面试题,题目是实现一个自定义的 Hook,实现点击切换状态。

function SomeComponent() {
  const [state, toggleState] = useToggle(false);
  return <div>
    {state ? 'true' : 'false'}
    <button onClick={toggleState}></button>
  </div>
}

// 请实现 useToggle
function useToggle(initialValue) {
    const [value, setValue] = useState(initialValue);
    const toggle = () => {setValue(!value)};
    return [value, toggle];
}
复制代码

七大 Hooks 都有哪些

  1. useState 状态
  2. useEffect 钩子,还有它的兄弟 useLayoutEffect
  3. useContext 上下文
  4. useReducer 代替 Redux
  5. useMemo 缓存,还有它的小弟 useCallback
  6. useRef 引用
  7. 自定义 Hook 混合

useState

基本语法: const [X, setX] = React.useState(X的初始值)

简单示例:

function App() {
  const [user,setUser] = useState({name:'Varian', age: 18})
  const onClick = ()=>{
    setUser({
      name: 'Janye'
    })
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}
复制代码

我们会发现,点击按钮之后,age 消失了,而我们明明只改了 name 呀,为什么呢?

简单来说就是前后是两个完全不相关的对象。

展开讲的话 React 在数据变化时会创建新的虚拟 DOM 对象,然后将这个虚拟 DOM 对象跟原虚拟 DOM 进行一个 DOM Diff,得到一个最小的变化过程 Patch,并把这个 Patch 渲染到页面上,Diff 的时候发现新对象没有 age 这个属性,于是就把它删除了。

于是在使用 useState 的时候我们需要注意两个地方:

  1. 想要原来的值,必须在 setX 里先进行复制,类似这样 setUser({...user, name: 'Janye'})
  2. setX(obj) 时,obj 的地址必须改变

useEffect

useEffect 的作用主要是用来解决函数组件如何像类组件一样使用生命周期钩子的问题。

它有三个使用场景:

  1. 作为 componentDidMount 使用,第二个参数为空数组 []
  2. 作为 componentDidUpdate 使用,第二个参数为指定依赖
  3. 作为 componentWillUnmount 使用,通过 return

这里给一个最简单的例子:

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);

  return (
    <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
  <BlinkyRender />,
  document.querySelector("#root")
);
复制代码

那么它跟它的兄弟 useLayoutEffect 有什么区别呢? useEffect 在浏览器渲染完成后执行,useLayoutEffect 在浏览器渲染前执行,useLayoutEffect 总是比 useEffect 先执行。

那么为了用户体验(先渲染就能先看到),通常我们应该先用useEffect

useContext

如果我们想在组件之间共享状态的话,可以使用 useContext

它的使用可以分为三个步骤:

  1. 使用C = createContext(initial) 创建上下文
  2. 使用<C.provider> 圈定作用域
  3. 在作用域内使用 useContext(C) 来使用上下文

简单示例:

const C = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    <C.Provider value={{ n, setN }}>
      <div className="App">
        <Baba />
      </div>
    </C.Provider>
  );
}

function Baba() {
  const { n, setN } = useContext(C);
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}

function Child() {
  const { n, setN } = useContext(C);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是儿子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}
复制代码

useReducer

如果要一句话解释 useReducer 的话,它是用来代替 Redux 的,或者说,是一个加强版的 useState

使用上来说,一共有四步:

  1. 创建初始值 initialState
  2. 创建所有操作 reducer(state, action)
  3. 传给 useReducer,得到读和写 API
  4. 调用 写({type: '操作类型'})

这里给一个基本的示例:

const initial = {
  n: 0
};

const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };
  const onClick2 = () => {
    dispatch({ type: "add", number: 2 });
  };
  return (
    <div className="App">
      <h1>n: {n}</h1>

      <button onClick={onClick}>+1</button>
      <button onClick={onClick2}>+2</button>
    </div>
  );
}
复制代码

useMemo

基本语法:useMemo(回调函数, [依赖])

类似与 Vue 的计算属性 computed,useMemo 具有缓存,依赖改变才重新渲染的功能。

跟它的小弟 useCallback 的唯一区别是:useMemo可以缓存所有对象,useCallback只能缓存函数。

useCallback(x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])

useRef

主要作用是创建一个数据的引用,并让这个数据在 render 过程中始终保持不变

基本语法: const count = useRef(0),读取用 count.current

用法这里给大家参考一下我封装 Echarts 时的例子:

export function ReactEcharts(props) {
  const {option, loading} = props
  const container = useRef(null)
  const chart = useRef(null)

  useEffect(() => {
    const width = document.documentElement.clientWidth
    const c = container.current
    console.log(c)
    c.style.width = `${width - 20}px`
    c.style.height = `${(width - 20) * 1.2}px`
    chart.current = echarts.init(c, 'dark')

  }, []) // [] - mounted on first time

  useEffect(() => {
    chart.current.setOption(option)
  }, [option]) // when option change 类似 vue 的 watch

  useEffect(() => {
    if (loading) chart.current.showLoading()
    else chart.current.hideLoading()
  }, [loading])
  return (
    <div ref={container}/>
  )
}
复制代码

自定义 Hook

可以理解为我们可以把上面的 Hook 按照实际的需求混合起来,封装成一个函数,给一个简单示例:

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    setList: setList
  };
};
export default useList;
复制代码

写在最后

结合最近参加面试的经历,有两个感想跟大家分享一下:

  1. 技术的学习和提高离不开持之以恒地练习,需要不断温故知新才能克服遗忘曲线;
  2. 利用好每次和面试官交流的机会,对于自己生疏的知识点进行及时反思复盘,进一步完善自己的知识体系。
分类:
前端