React Hooks 各个击破

852 阅读4分钟

React Hooks

  • 状态 usestate
  • 副作用 useEffect
  • 上下文 useContext
  • Redux useReducer
  • 记忆 useMemo
  • 引用 useRef
  • 自定义Hook

useState

使用状态

const [n,setN] = React.useState(0)
const [user,setUser] = React.useState({name:'xiaoming'})

不可局部更新

如果state是一个对象,是不能部分 setState 的。

当点击按钮

age 就会消失。

因为setState 不会帮我们合并属性

可采用这种方法:

地址要变

setState(obj) ,如果obj地址不变,那么 React 就认为数据没有变化。

useState 可接受函数

当初始值比较复杂时,可采用。

const [state,setState] = useState(()=>{
    return initialState
})

该函数返回初始 state ,且只执行一次。

setState 接受函数

setN(i=>i+1)

什么时候采用这种方式呢?

import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
    setN(n+1)   // setN(n+1) 并不会使得 n 改变
    setN(n+1) // 你会发现 n 不能加 2
    // setN(i=>i+1)
    // setN(i=>i+1) // 采用这种方式就可实现加2
  }
  return (
    <div className="App">
      <h1>n: {n}</h1>
       
      <button onClick={onClick}>+2</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

其实应该使用这种形式。

useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法

import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";

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>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

总的来说,useReducer 是 useState 的复杂版

useContext

  • 使用 C = createContext(initial) 创建上下问
  • 使用 <C.provider> 圈定作用域
  • 在作用域内 使用useContext(C) 来使用上下问
import React, { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

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>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useEffect

副作用

  • 对环境的改变即为副作用,如修改 document.title
  • 不一定非要把副作用放在 useEffect 里
  • 实际上叫做 afterRender 可能更好,每次 render 后执行

用途

  • 作为componentDidMount 使用,[]作第二个参数
  • 作为 componentDidUpdate 使用, 可指定依赖
  • 作为componentWillUnmount 使用,通过 return
  • 以上三种用途可以同时存在

特点

如果同时存在多个 useEffect ,会按照出现次序执行。

useLayoutEffect

useEffect 在浏览器渲染完成后执行

useLayoutEffect 在浏览器渲染前执行

特点

  • useLayoutEffect 总是比 useEffect 先执行
  • useLayoutEff 里的任务最好影响了 Layout

为了用户体验,优先使用 useEffect

useMemo

React.memo

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>  
      </div>
      <Child data={m}/>
      {/* <Child2 data={m}/> */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React默认有多余的render,点击按钮,Child() 执行了,但Child 依赖数据并没有改变,此时,可使用React.memo(Child)代替 Child。

如果props不变,就没必要再执行一个函数组件。

但是,React.memo有个 bug

const onClickChild = ()=>{}

把一个监听函数传给这个组件时,即使监听函数什么也不做,每次当外部组件数据改变重新渲染时,这个组件也会执行。

这是因为每次重新执行 App() ,都会生成一个新的监听函数,和之前的监听函数地址不同,所以会导致这个组件也执行。


使用useMemo可以解决这个问题

const onClickChild = useMemo(()=>{ return console.log(m) },[m])

useMemo特点

  • 第一个参数是 ()=> value
  • 第二个参数是依赖[m,n]
  • 只有当依赖变化时,才会计算出新的 value,如果依赖不变,那么就重用之前的value

注意

如果你的 value 是个函数,那么就要写成 useMemo(()=> ()=> console.log(x))

这是一个返回函数的函数,很难用,于是有了 useCallback

useCallback

用法

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

useRef

目的

  • 如果需要一个值,在组件不断 render 时保持不变
  • 初始化: const count=useRef(0)
  • 读取:count.current
  • 为什么需要current ,为了保证两次useRef是同一个值(只有引用能做到)

forwardRef

props无法传递ref属性。

import React, { useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}

const Button3 = React.forwardRef((props, ref) => {
  return <button className="red" ref={ref} {...props} />;
});

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useImperativeHandle

用于自定义 ref 的属性

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
  const setRef = useImperativeHandle;
  setRef(ref, () => {
    return {
      x: () => {
        realButton.current.remove();
      },
      realButton: realButton
    };
  });
  return <button ref={realButton} {...props} />;
});

自定义 Hook

封装数据操作

hooks/useList.js

import { useState, useEffect } from "react";

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

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" },
        { id: 3, name: "Alice" },
        { id: 4, name: "Bob" }
      ]);
    }, 2000);
  });
}

index.js

import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, setList } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);