关于react-hooks:React-Hook丨用好这9个钩子所向披靡

25次阅读

共计 7111 个字符,预计需要花费 18 分钟才能阅读完成。

Hook 进去后,置信很多小伙伴都本人蠢蠢欲动,对于喜爱用 react 的,又喜爱 Hook 的,本篇文章将会与你一起玩转 Hook。

文章篇幅有一丢丢长,但请急躁品它 ~

钩子函数:某个阶段触发的回调函数。
例:vue 的生命周期函数就是钩子函数

工欲善其事,必先利其器

让咱们先深刻理解 react 内置的这几个钩子

这里咱们简略给几个钩子贴上标签

  1. useState【保护状态】
  2. useEffect【实现副作用操作】
  3. useContext【应用共享状态】
  4. useReducer【相似 redux】
  5. useCallback【缓存函数】
  6. useMemo【缓存值】
  7. useRef【拜访 DOM】
  8. useImperativeHandle【应用子组件裸露的值 / 办法】
  9. useLayoutEffect【实现副作用操作,会阻塞浏览器绘制】

接下来,咱们来针对这 9 个钩子一一深刻理解

useState

一般更新 / 函数式更新 state

const Index = () => {const [count, setCount] = useState(0);
  const [obj, setObj] = useState({id: 1});
  return (
    <>
      {/* 一般更新 */}
      <div>count:{count}</div>
      <button onClick={() => setCount(count + 1)}>add</button>

      {/* 函数式更新 */}
      <div>obj:{JSON.stringify(obj)}</div>
      <button
        onClick={() =>
          setObj((prevObj) => ({...prevObj, ...{ id: 2, name: "张三"} }))
        }
      >
        merge
      </button>
    </>
  );
};

useEffect

useEffet 咱们能够了解成它替换了componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期,然而它的性能还更弱小。

这个钩子比拟重要,咱们花点工夫来把握它。

  1. 蕴含 3 个生命周期的代码构造
useEffect(() => {
    // 这里的代码块 等价于 componentDidMount
    // do something...

    // return 的写法 等价于 componentWillUnmount 
    return () => {// do something...};
  },
  // 依赖列表,当依赖的值有变更时候,执行副作用函数,等价于 componentDidUpdate
  [xxx,obj.xxx]
);

留神:依赖列表是灵便的,有三种写法

  • 当数组为空 [],示意不会应为页面的状态扭转而执行回调办法【即仅在初始化时执行,componentDidMount】,
  • 当这个参数不传递,示意页面的任何状态一旦变更都会执行回调办法
  • 当数组非空,数组里的值一旦有变动,就会执行回调办法
  1. 咱们还会遇到一些场景,如:
  • 场景 1:我依赖了某些值,然而我不要在初始化就执行回调办法,我要让依赖扭转再去执行回调办法

咱们这里有用到了 useRef 这个钩子:

const firstLoad = useRef(true);
useEffect(() => {if (firstLoad.current) {
    firstLoad.current = false;
    return;
  }
  // do something...
}, [xxx]);
  • 场景 2:我有一个 getData 的异步申请办法,我要让其在初始化调用且点击某个按钮也能够调用

咱们先这样写

// ...
  const getData = async () => {const data = await xxx({ id: 1});
    setDetail(data);
  };

  useEffect(() => {getData();
  }, []);

  const handleClick = () => {getData();
  };
// ...

然而报了个 warning:

Line 77:6:  React Hook useEffect has a missing dependency: 'getData'. 
Either include it or remove the dependency array 
react-hooks/exhaustive-deps

报错的意思就是:我须要 useEffect 须要增加 getData 依赖

这是 Hook 的规定,于是咱们这样改:

// ...
  const getData = async () => {const data = await xxx({ id: 1});
    setDetail(data);
  };

  useEffect(() => {getData();
  }, [getData]);

  const handleClick = () => {getData();
  };
// ...

然而又报了个 warning:

Line 39:9:  The 'getData' function makes the dependencies of useEffect Hook (at line 76) change on every render. 
Move it inside the useEffect callback. 
Alternatively, wrap the 'getData' definition into its own useCallback() Hook  react-hooks/exhaustive-deps

报错的意思就是:这个组件只有一有更新触发了 render,getData 的就会从新被定义,此时的援用不一样,会导致 useEffect 运行。

这个是影响性能的行为,咱们用 useCallback 钩子来缓存它来进步性能:

// ...
  const getData = useCallback(async () => {const data = await xxx({ id: 1});
    setDetail(data);
  }, []);

  useEffect(() => {getData();
  }, [getData]);

  const handleClick = () => {getData();
  };
// ...

这只是一个例子,次要是为了阐明 依据谬误提醒,从而引起的思考。当然你也能够用一个正文来敞开 eslint,或者间接敞开 eslint 规定,这次要看你的取舍。

应用 // eslint-disable-next-line react-hooks/exhaustive-deps,如:

// ...
  const [count, setCount] = useState(1);
  const xxx = () => {};
  useEffect(() => {
    // use count do something...
    console.log(count);    

    xxx();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
// ...

useContext

Context 提供了一种在组件之间共享此类值的形式,而不用显式地通过组件树
的逐层传递 props

一个例子阐明

const obj = {value: 1};
const obj2 = {value: 2};

const ObjContext = React.createContext(obj);
const Obj2Context = React.createContext(obj2);

const App = () => {
  return (<ObjContext.Provider value={obj}>
      <Obj2Context.Provider value={obj2}>
        <ChildComp />
      </Obj2Context.Provider>
    </ObjContext.Provider>
  );
};
// 子级
const ChildComp = () => {return <ChildChildComp />;};
// 孙级或更多级
const ChildChildComp = () => {const obj = useContext(ObjContext);
  const obj2 = useContext(Obj2Context);
  return (
    <>
      <div>{obj.value}</div>
      <div>{obj2.value}</div>
    </>
  );
};

useReducer

在某些场景下,useReducer 会比 useState 更实用,当 state 逻辑较简单。咱们就能够用这个钩子来代替 useState,它的工作形式犹如 Redux,看一个例子:

const initialState = [{ id: 1, name: "张三"},
  {id: 2, name: "李四"}
];

const reducer = (state: any, { type, payload}: any) => {switch (type) {
    case "add":
      return [...state, payload];
    case "remove":
      return state.filter((item: any) => item.id !== payload.id);
    case "update":
      return state.map((item: any) =>
        item.id === payload.id ? {...item, ...payload} : item
      );
    case "clear":
      return [];
    default:
      throw new Error();}
};

const List = () => {const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      List: {JSON.stringify(state)}
      <button
        onClick={() =>
          dispatch({type: "add", payload: { id: 3, name: "周五"} })
        }
      >
        add
      </button>

      <button onClick={() => dispatch({ type: "remove", payload: { id: 1} })}>
        remove
      </button>

      <button
        onClick={() =>
          dispatch({type: "update", payload: { id: 2, name: "李四 -update"} })
        }
      >
        update
      </button>
      
      <button onClick={() => dispatch({ type: "clear"})}>clear</button>
    </>
  );
};

裸露进来的 type 能够让咱们更加的理解,当下咱们正在做什么事。

useCallback

// 除非 `a` 或 `b` 扭转,否则不会变
const memoizedCallback = useCallback(() => {doSomething(a, b);
  },
  [a, b],
);

入手滑到下面,曾经有提到了一个例子,说到了 useCallback,算是一个场景,
咱们都晓得它能够用来缓存一个函数。

接下来咱们讲讲另一个场景。

后面讲的话:react 中只有父组件的 render 了,那么默认状况下就会触发子组
的 render,react 提供了来防止这种重渲染的性能开销的一些办法:
React.PureComponentReact.memo shouldComponentUpdate()

让咱们来急躁看一个例子,当咱们子组件承受属性是一个办法的时候,如:

const Index = () => {const [count, setCount] = useState(0);

  const getList = (n) => {return Array.apply(Array, Array(n)).map((item, i) => ({
      id: i,
      name: "张三" + i
    }));
  };

  return (
    <>
      <Child getList={getList} />
      <button onClick={() => setCount(count + 1)}>count+1</button>
    </>
  );
};

const Child = ({getList}) => {console.log("child-render");
  return (
    <>
      {getList(10).map((item) => (<div key={item.id}>
          id:{item.id},name:{item.name}
        </div>
      ))}
    </>
  );
};

咱们来尝试解读一下,当点击“count+1”按钮,产生了这样子的事:

父组件 render > 子组件 render > 子组件输入 "child-render"

咱们为了防止子组件做没必要的渲染,这里用了React.memo,如:

// ...
const Child = React.memo(({getList}) => {console.log("child-render");
  return (
    <>
      {getList(10).map((item) => (<div key={item.id}>
          id:{item.id},name:{item.name}
        </div>
      ))}
    </>
  );
});
// ...

咱们不假思索的认为,当咱们点击“count+1”时,子组件不会再重渲染了。但事实
是,还是仍然会渲染,这是为什么呢?
答:Reace.memo 只会对 props 做浅比拟,也就是父组件从新 render 之后会传入
不同援用的办法 getList,浅比拟之后不相等,导致子组件还是仍然会渲染。

这时候,useCallback 就能够上场了,它能够缓存一个函数,当依赖没有扭转的时候,会始终返回同一个援用。如:

// ...
const getList = useCallback((n) => {return Array.apply(Array, Array(n)).map((item, i) => ({
    id: i,
    name: "张三" + i
  }));
}, []);
// ...

总结:如果子组件承受了一个办法作为属性,咱们在应用 React.memo 这种防止子组件做没必要的渲染时候,就须要用 useCallback 进行配合,否则 React.memo 将无意义。

useMemo

与 vue 的 computed 相似,次要是用来防止在每次渲染时都进行一些高开销的计算,举个简略的例子。

不论页面 render 几次,工夫戳都不会被扭转,因为曾经被被缓存了,除非依赖扭转。

// ...
const getNumUseMemo = useMemo(() => {return `${+new Date()}`;
}, []);
// ...

useRef

咱们用它来拜访 DOM,从而操作 DOM,如点击按钮聚焦文本框:

const Index = () => {const inputEl = useRef(null);
  const handleFocus = () => {inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={handleFocus}>Focus</button>
    </>
  );
};

留神:返回的 ref 对象在组件的整个生命周期内放弃不变。
它相似于一个 class 的实例属性,咱们利用了它这一点。
入手滑到下面再看下面看那个有 useRef 的例子。

刚刚举例的是拜访 DOM,那如果咱们要拜访的是一个组件,操作组件里的具体 DOM 呢?咱们就须要用到 React.forwardRef 这个高阶组件,来转发 ref,如:

const Index = () => {const inputEl = useRef(null);
  const handleFocus = () => {inputEl.current.focus();
  };
  return (
    <>
      <Child ref={inputEl} />
      <button onClick={handleFocus}>Focus</button>
    </>
  );
};

const Child = forwardRef((props, ref) => {return <input ref={ref} />;
});

useImperativeHandle

useImperativeHandle 能够让咱们在父组件调用到子组件裸露进去的属性 / 办法。如:

const Index = () => {const inputEl = useRef();
  useEffect(() => {console.log(inputEl.current.someValue);
    // test
  }, []);

  return (
    <>
      <Child ref={inputEl} />
      <button onClick={() => inputEl.current.setValues((val) => val + 1)}>
        累加子组件的 value
      </button>
    </>
  );
};

const Child = forwardRef((props, ref) => {const inputRef = useRef();
  const [value, setValue] = useState(0);
  useImperativeHandle(ref, () => ({
    setValue,
    someValue: "test"
  }));
  return (
    <>
      <div>child-value:{value}</div>
      <input ref={inputRef} />
    </>
  );
});

总结:相似于 vue 在组件上用 ref 标记,而后 this.$refs.xxx 来操作 dom 或者调用子组件值 / 办法,只是 react 把它“用两个钩子来示意”。

useLayoutEffect

在所有的 DOM 变更之后同步调用 effect。能够应用它来读取 DOM 布局并同步
触发重渲染。在浏览器执行绘制之前,useLayoutEffect 外部的更新打算将被同
步刷新,也就是说它会阻塞浏览器绘制。所以尽可能应用 useEffect 以防止阻
塞视觉更新。

点我,看个例子,豁然开朗

大总结:接下来的前端时代将是 Hook 的时代,喜爱 react 的同学,让咱们先用好这几个钩子,所向无敌。

正文完
 0