React Hook丨用好这9个钩子,所向披靡

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 我们可以理解成它替换了componentDidMount, componentDidUpdate, componentWillUnmount 这三个生命周期,但是它的功能还更强大。

这个钩子比较重要,我们花点时间来掌握它。

  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.memoshouldComponentUpdate()

让我们来耐心看一个例子,当我们子组件接受属性是一个方法的时候,如:

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的同学,让我们先用好这几个钩子,所向披靡。

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

推荐阅读更多精彩内容