React – Hooks 常识系统概览

  • 起步:我的Hooks 学习材料
  • 开端Hooks 发生的原因和发生背景
  • Hooks Api:

    • 状况钩子:useStateuseReducer
    • 副作用钩子: useEffect,useLayoutEffect
    • 同享状况钩子: useCotext
    • 回忆值钩子: useMemo
    • 回忆回调函数钩子: useCallback
    • ref 钩子: useRef
    • ImperativeHandle钩子:useImperativeHandle
  • React Hooks in TypeScript
  • React Hooks 造轮子

    • DOM 副作用修正 / 监听
    • 组件辅佐
    • 动画
    • 恳求
    • 表单
    • 模仿生命周期
    • 存数据
    • 封装原有库
  • React Hooks 源码:待续

起步

我的 Hook 学习材料

Hooks Start

React Hooks Tutorial

useState – useReducer

how to useState in React

useState – add-item-to-list

Remove an Item from a List in React

How to update Item from a List in React

useEffect – useLayoutEffect

How to useEffect in React

How to fetchData in useEffect

A complete Guide to useEffect

using the effect Hook

React useLayoutEffect vs. useEffect with examples

useContext

How to use React Context

How to useContext in React

useMemo

【译】什么时分运用 useMemo 和 useCallback

docs useMemo

useCallback

【译】什么时分运用 useMemo 和 useCallback

docs useCallback

如何錯誤地运用 React hooks useCallback 來保存相同的 function instance

useRef

How to use React Ref

docs useRef

React: Using Refs with the useRef Hook

useImperativeHandle

docs useImperativeHandle

React Hooks in TypeScript

React Hooks in TypeScript

React Hooks 造轮子

react-use

精读《怎样用 React Hooks 造轮子》

开端

Hooks 发生的原因,背景和意图

Hooks 发生的背景和意图

React Hooks 是在 2018 年 10 月React Conf 上引入的,替换原先类组件的书写方法,使得函数式组件支撑 stateside-effectsReact-Hooks 之前的函数式组件是无状况组件,所以咱们能够说 React-Hooks 就是增强的函数式组件,支撑状况和副作用的全功能组件。

Hooks 发生的原因

  • 函数式组件愈加优雅,愈加轻量级,经过一个小计数器 demo 就能够发现。前者是 Hooks 之前的类组件,后者式函数式组件
// Hooks之前的类组件
import React, { Component } from "react";
class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
export default Counter;
// Hooks
import React from "react";
function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Counter;
  • React 开发人员的学习曲线不会很峻峭,只需求知晓 state, side-effects 概念就能够,不需求在像学习类组件的时分,需求知道 Class, this 绑定,super,state, setState, render 函数,生命周期。根本消除了带给 React 初学者的挫败感。

  • React 核心成员说的那样,Hooks 消除了 wrapper hell – 包装阴间,我就遇到过这种包装阴间场景:其时的状况是一个类组件,包装了 redux 供给的 connect函数,包装国际化库供给的 Api函数,包装自己封装的高阶组件函数,还包装了withRouter函数。这种代码可读性极差,层级嵌套的代码像个怪物。

useState


根本介绍

为函数组件引入 state,以及更新statesetState 函数。

Tips:与 class 组件傍边 setState 不同,HooksetState不会自动兼并更新目标,所以咱们会运用打开运算符,结合 HooksetState 来到达更新目标的意图。


根本运用

  • 传入初始状况,回来一个数组,数组的第一个成员是 state,第二个成员是改动 state 的函数。

useState 操练:歌曲的增删改

import { useState } from "react";
const InitialData = [{ name: "歌名1", id: 1, isComplete: false }];
export const HookUseStateCpn = () => {
  const [songState, setSongState] = useState(InitialData);
  const [songName, setSongName] = useState("");
  const setName = (e) => {
    setSongName(e.target.value);
  };
  /**
   * 添加歌曲处理
   */
  const handleAddSong = () => {
    const newData = songState.concat({
      name: songName,
      id: songState.length + 1,
      isComplete: false,
    });
    setSongState(newData);
    setSongName("");
  };
  /**
   * 删去歌曲处理
   * @param {*} id 歌曲 id
   */
  const handleDeleteSong = (id) => {
    const newData = songState.filter((item) => item.id !== id);
    setSongState(newData);
  };
  /**
   * 修改歌曲处理
   * @param {*} id 歌曲id
   */
  const handleEditSong = (id) => {
    const newData = songState.map((item, index) => {
      if (item.id === id) item.isComplete = !item.isComplete;
      return item;
    });
    setSongState(newData);
  };
  return (
    <>
      <List
        songState={songState}
        handleDeleteSong={handleDeleteSong}
        handleEditSong={handleEditSong}
      />
      <AddItem
        setName={setName}
        handleAddSong={handleAddSong}
        songName={songName}
      />
    </>
  );
};
// 子组件 AddItem
const AddItem = ({ setName, handleAddSong, songName }) => {
  return (
    <>
      <input onChange={(e) => setName(e)} value={songName}></input>
      <button onClick={(e) => handleAddSong(e)}>添加</button>
    </>
  );
};
// 子组件 List
const List = ({ songState, handleDeleteSong, handleEditSong }) => {
  return (
    <>
      <h1>最喜欢的歌</h1>
      {songState.map((song) => {
        return (
          <div key={song.id}>
            <h4
              style={{
                textDecoration: song.isComplete ? "line-through" : "none",
              }}
            >
              {song.name} -- {song.id}{" "}
              <button onClick={() => handleEditSong(song.id)}>
                {song.isComplete ? "吊销完结" : "完结"}
              </button>
              <button onClick={() => handleDeleteSong(song.id)}>删去</button>
            </h4>
          </div>
        );
      })}
    </>
  );
};

useReducer


根本介绍

咱们说,useStateReact-Hooks 引入了状况办理,useReducer 的作用也是为 React-Hooks 供给了状况办理。useReduceruseState 的增强和替代 API,useReducer 用于杂乱状况办理和功能优化, 功能优化是因为它能够凭借 Context API 传递 dispatch 函数给子组件,因为dispatch 函数始终不变,所以子组件不会从头烘托。

Tips:关于运用或者把握了 Redux 的开发者来讲,useReducer 运用起来会很顺手。因为其间像:storereducer,state,dispatch,action的概念现已把握和了解了。知道 store 的更新流程是怎样样的,运用 useReducer 自然会愈加顺手一些


根本运用

import { useReducer, useState } from "react";
const InitialData = [{ name: "歌名1", id: 1, isComplete: false }];
const SongReducer = (state, action) => {
  const id = action?.payload?.id;
  switch (action.type) {
    case "ADD_SONG":
      return [...state, { name: action.payload.name, id, isComplete: false }];
    case "DELETE_SONG":
      return state.filter((item) => {
        return item.id !== id;
      });
    case "EDIT_SONG":
      return state.map((item) => {
        if (item.id === id) {
          const updateItem = {
            ...item,
            isComplete: !action.payload.isComplete,
          };
          return updateItem;
        }
        return item;
      });
    default:
      return new Error();
  }
};
export const HookUseReducerCpn = () => {
  const [songReducerState, dispatchSongData] = useReducer(
    SongReducer,
    InitialData
  );
  const [songName, setSongName] = useState("");
  const setName = (e) => {
    setSongName(e.target.value);
  };
  /**
   * 添加歌曲处理
   */
  const handleAddSong = () => {
    dispatchSongData({
      type: "ADD_SONG",
      payload: { name: songName, id: songReducerState.length + 1 },
    });
    setSongName("");
  };
  /**
   * 删去歌曲处理
   * @param {*} id 歌曲 id
   */
  const handleDeleteSong = (id) => {
    dispatchSongData({ type: "DELETE_SONG", payload: { id } });
  };
  /**
   * 修改歌曲处理
   * @param {*} id 歌曲id
   */
  const handleEditSong = (id, isComplete) => {
    dispatchSongData({ type: "EDIT_SONG", payload: { id, isComplete } });
  };
  return (
    <>
      <List
        songState={songReducerState}
        handleDeleteSong={handleDeleteSong}
        handleEditSong={handleEditSong}
      />
      <AddItem
        setName={setName}
        handleAddSong={handleAddSong}
        songName={songName}
      />
    </>
  );
};
const AddItem = ({ setName, handleAddSong, songName }) => {
  return (
    <>
      <input onChange={(e) => setName(e)} value={songName}></input>
      <button onClick={(e) => handleAddSong(e)}>添加</button>
    </>
  );
};
const List = ({ songState, handleDeleteSong, handleEditSong }) => {
  return (
    <>
      <h1>最喜欢的歌</h1>
      {songState.map((song) => {
        return (
          <div key={song.id}>
            <h4
              style={{
                textDecoration: song.isComplete ? "line-through" : "none",
              }}
            >
              {song.name} -- {song.id}{" "}
              <button onClick={() => handleEditSong(song.id, song.isComplete)}>
                {song.isComplete ? "吊销完结" : "完结"}
              </button>
              <button onClick={() => handleDeleteSong(song.id)}>删去</button>
            </h4>
          </div>
        );
      })}
    </>
  );
};

useEffect


根本介绍

  • useEffectReact 引入了副作用函数,你能够运用 useEffect 各种用法来决定,副作用函数在组件挂载时,组件烘托时仍是在组件更新烘托时运转,你能够在副作用函数傍边,进行 mutations (状况改动), subscriptions(添加订阅), timers(设置定时器),logging(添加日志),fetch data(网络恳求)。
每一次组件的烘托都有对应这一次烘托 stateprops

在开端聊 useEffect 之前,咱们先来聊一聊组件的烘托,咱们首先要明确,每一次组件的烘托都有对应这一次 stateprops,经过下面的定时器事例,你会对此有更深的了解。

// 这是一个简略的计数器事例
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

当咱们点击按钮改动组件状况的时分,React 会从头烘托组件,每一次烘托都会从头创立组件函数,拿到归于本次的 count 状况,所以这个 count 值只是函数作用域傍边的一个常量。经过下面的代码,你会对此有愈加深化的了解。

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}
// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}
// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>;
  // ...
}

所以计数器事例中的 count 没有什么魔法(magic),只是一个常量,也没有像 vue 傍边的 watcherproxy,或者是 databinding

每一次组件烘托,都有对应这一次烘托的作业处理函数

咱们明确了每一次组件的烘托都有对应这一次 stateprops之后,第二个要明确的是每一次组件烘托,都有对应这一次的作业处理函数。经过下面的事例,你会对此有更深的了解。

function Counter() {
  const [count, setCount] = useState(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}
  • 点击添加 counter 到 3
  • 点击一下 “Show alert”
  • 点击添加 counter 到 5 并且在定时器回调触发前完结

最终的成果竟然是 3 而不是 5,下面的代码会让你有愈加深入的了解。

// During first render
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }
  // ...
}
// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }
  // ...
}
// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }
  // ...
}

所以实际上,每一次烘托都有一个“新版别”handleAlertClick(作业处理函数)。每一个版别的 handleAlertClick(作业处理函数)“记住” 了它自己本次对应的 count

每一次组件烘托都有对应这一次烘托的 Effects

这是一个简略的 useEffect 事例

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

每一次组件的烘托,都有对应这一次烘托的 Effects ,下面的代码会让你有愈加深入的了解

// During first render
function Counter() {
  // ...
  useEffect(
    // Effect function from first render
    () => {
      document.title = `You clicked ${0} times`;
    }
  );
  // ...
}
// After a click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from second render
    () => {
      document.title = `You clicked ${1} times`;
    }
  );
  // ...
}
// After another click, our function is called again
function Counter() {
  // ...
  useEffect(
    // Effect function from third render
    () => {
      document.title = `You clicked ${2} times`;
    }
  );
  // ..
}

React 会记住你供给的 effects 函数,等候每次 ReactDOM 更新烘托制作到 screen 上之后去调用 effects 函数,本质上,effects 函数每次“看到的”都是对应那一次烘托的 stateprops.

useEffect 回来的整理函数是怎样回事
useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});

假设第一次烘托的时分 props 是{id: 10},第二次烘托的时分是{id: 20}

  • 会发生下面的作业:
    • React 烘托{id: 20}的 UI。
    • 浏览器制作。咱们在屏幕上看到{id: 20}的 UI。
    • React 铲除{id: 10}的 effect。
    • React 运转{id: 20}的 effect。
向 useEffects 中传递依靠

上方的 useEffect 事例,只要一个函数作为参数,这样的成果是在组件每次烘托时都会调用 effects 函数,这样并不高效,还经常导致无限烘托问题,所以咱们需求传递第二参数来处理问题,第二参数是一个数组依靠项,只要当依靠项发生改动时, 才会调用 effects 函数.

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // Our deps

只要当 name 发生改动时才会调用 effects 函数.

削减 useEffects 傍边的依靠性来到达功能优化
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

现在依靠数组正确了。尽管它可能不是太理想但的确处理了上面的问题。现在,每次 count 修正都会从头运转 effect,并且定时器中的 setCount(count + 1)会正确引证某次烘托中的 count 值:

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
        setCount(0 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [0] // [count]
  );
  // ...
}
// Second render, state is 1
function Counter() {
  // ...
  useEffect(
    // Effect from second render
    () => {
      const id = setInterval(() => {
        setCount(1 + 1); // setCount(count + 1)
      }, 1000);
      return () => clearInterval(id);
    },
    [1] // [count]
  );
  // ...
}

这能处理问题可是咱们的定时器会在每一次 count 改动后铲除和从头设定。这应该不是咱们想要的成果.所以咱们需求削减依靠项的一起,满意咱们的需求.

削减依靠计划一:seState 的 updater function
useEffect(() => {
  const id = setInterval(() => {
    setCount((c) => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
削减依靠计划二:useReducer 的 dispatch function
function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);
  function reducer(state, action) {
    if (action.type === "tick") {
      return state + step;
    } else {
      throw new Error();
    }
  }
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: "tick" });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return <h1>{count}</h1>;
}
削减依靠计划三:将函数界说到 effects 函数傍边
function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside!
    function getFetchUrl() {
      return "https://hn.algolia.com/api/v1/search?query=react";
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }
    fetchData();
  }, []); // ✅ Deps are OK
  // ...
}
削减依靠计划四:将函数界说到 effects 函数外面

针关于第三种处理计划,缺点是不能复用 getFetchUrl 函数,假如多个地方一起运用到了该函数,只能每次都从头界说.

function SearchResults() {
  //  Re-triggers all effects on every render
  function getFetchUrl(query) {
    return "https://hn.algolia.com/api/v1/search?query=" + query;
  }
  useEffect(() => {
    const url = getFetchUrl("react");
    // ... Fetch data and do something ...
  }, [getFetchUrl]); //  Deps are correct but they change too often
  useEffect(() => {
    const url = getFetchUrl("redux");
    // ... Fetch data and do something ...
  }, [getFetchUrl]); //  Deps are correct but they change too often
  // ...
}

两个更简略的处理办法:

  • 假如该函数没有依靠任何组件状况,放到组件的外面傍边去.
// ✅ Not affected by the data flow
function getFetchUrl(query) {
  return "https://hn.algolia.com/api/v1/search?query=" + query;
}
function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl("react");
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK
  useEffect(() => {
    const url = getFetchUrl("redux");
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK
  // ...
}
  • 假如该函数依靠了组件状况,那就放在组件傍边,运用 callback 进行包裹.
function SearchResults() {
  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return "https://hn.algolia.com/api/v1/search?query=" + query;
  }, []); // ✅ Callback deps are OK
  useEffect(() => {
    const url = getFetchUrl("react");
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK
  useEffect(() => {
    const url = getFetchUrl("redux");
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK
  // ...
}

根本运用


向服务器恳求数据(fetch data) – useState

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [url]);
  return [{ data, isLoading, isError }, setUrl];
};

向服务器恳求数据(fetch data) – useReducer

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false,
      };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};
const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  useEffect(() => {
    let didCancel = false;
    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });
      try {
        const result = await axios(url);
        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };
    fetchData();
    return () => {
      didCancel = true;
    };
  }, [url]);
  return [state, setUrl];
};

useEffects: 第一次烘托(挂载)后和每一次的组件更新都会履行

— meaning it runs on the first render of the component (also called on mount or mounting of the component) and on every re-render of the component (also called on update or updating of the component).

const Toggler = ({ toggle, onToggle }) => {
  React.useEffect(() => {
    console.log("I run on every render: mount + update.");
  });
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
      {toggle && <div>Hello React</div>}
    </div>
  );
};

useEffect:只要在第一次烘托(挂载)后履行

If the dependency array is empty, the side-effect function used in React’s useEffect Hook has no dependencies, meaning it runs only the first time a component renders(mount).

const Toggler = ({ toggle, onToggle }) => {
  React.useEffect(() => {
    console.log("I run only on the first render: mount.");
  }, []);
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
      {toggle && <div>Hello React</div>}
    </div>
  );
};

useEffect: 组件更新的时分履行,包含第一次烘托(挂载)

Now the side-effect function for this React component runs only when the variable in the dependency array changes. However, note that the function runs also on the component’s first render (mount).

const Toggler = ({ toggle, onToggle }) => {
  const [title, setTitle] = React.useState("Hello React");
  React.useEffect(() => {
    console.log("I run if toggle or title change (and on mount).");
  }, [toggle, title]);
  const handleChange = (event) => {
    setTitle(event.target.value);
  };
  return (
    <div>
      <input type="text" value={title} onChange={handleChange} />
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
      {toggle && <div>{title}</div>}
    </div>
  );
};

useEffect: 只在组件更新的时分履行,第一次烘托(挂载)不履行

const Toggler = ({ toggle, onToggle }) => {
  const didMount = React.useRef(false);
  React.useEffect(() => {
    if (didMount.current) {
      console.log("I run only if toggle changes.");
    } else {
      didMount.current = true;
    }
  }, [toggle]);
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
      {toggle && <div>Hello React</div>}
    </div>
  );
};

useEffect: 只在组件更新的时分履行一次,第一次烘托(挂载)不履行

const Toggler = ({ toggle, onToggle }) => {
  const calledOnce = React.useRef(false);
  React.useEffect(() => {
    if (calledOnce.current) {
      return;
    }
    if (toggle === false) {
      console.log("I run only once if toggle is false.");
      calledOnce.current = true;
    }
  }, [toggle]);
  return (
    <div>
      <button type="button" onClick={onToggle}>
        Toggle
      </button>
      {toggle && <div>Hello React</div>}
    </div>
  );
};

useEffect: 整理回调函数

import * as React from "react";
const App = () => {
  const [timer, setTimer] = React.useState(0);
  React.useEffect(() => {
    const interval = setInterval(() => setTimer(timer + 1), 1000);
    return () => clearInterval(interval);
  }, [timer]);
  return <div>{timer}</div>;
};
export default App;

useLayoutEffect

根本介绍

useLayoutEffectuseEffect 有着相同的函数签名,两者在大多数状况下能够彼此替换,useLayoutEffectuseEffect的本质区别是触发时机的不一致,useEffect 发生在将 DOM 烘托制作到屏幕之后,而 useLayoutEffect 发生在 DOM 烘托制作到屏幕之前同步触发.也就是useLayoutEffectuseEffect更快履行.useLayouEffect用在杂乱的动画傍边,作用比 useEffect 愈加洁净(没有闪烁(flicker)的状况).当然大多数状况下,仍是运用 useEffect,因为 useEffect 先将 DOM 改动(mutation)进行制作,后履行effects 函数,而 useLayoutEffect 是先核算 effects 函数再制作,核算的进程会在一定程度上阻塞浏览器的烘托.

The useLayoutEffect function is triggered synchronously before the DOM mutations are painted. However, the useEffect function is called after the DOM mutations are painted.

useContext


根本介绍

useContext 处理垂直方向上嵌套层级太深的组件传递 props 太冗长的问题,该问题也被叫做 props drilling 问题。

  +----------------+
          |                |
          |        A       |
          |        |Props  |
          |        v       |
          |                |
          +--------+-------+
                   |
         +---------+-----------+
         |                     |
         |                     |
+--------+-------+    +--------+-------+
|                |    |                |
|                |    |        +       |
|       B        |    |        |Props  |
|                |    |        v       |
|                |    |                |
+----------------+    +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |        +       |
                      |        |Props  |
                      |        v       |
                      |                |
                      +--------+-------+
                               |
                      +--------+-------+
                      |                |
                      |        +       |
                      |        |Props  |
                      |        C       |
                      |                |
                      +----------------+

根本运用

import * as React from "react";
import { createContext, useState, useContext } from "react";
import ReactDOM from "react-dom";
const genColor = () => `hsla(${Math.random() * 360}, 70%, 50%)`;
type MyContext = { color: string, changer: (any) => void };
const ThemeContext = createContext < MyContext > null;
const Comp = () => {
  const { color, changer } = useContext(ThemeContext);
  return (
    <button style={{ color, fontSize: "2em" }} onClick={changer}>
      Hello World! Click to change color
    </button>
  );
};
const App = () => {
  const [state, setState] = useState({ color: "green" });
  const { color } = state;
  const changer = () => setState({ color: genColor() });
  return (
    <ThemeContext.Provider value={{ color, changer }}>
      <Comp />
    </ThemeContext.Provider>
  );
};

useContext 最佳实践

封装useContext独自成为一个context文件,这里是currencyContext.js。将context.Providercontext数据改动context目标的回调函数封装到一个文件,导出一个封装Provider在顶层的高阶组件(这里是CurrencyProvider)和一个归于该context目标的自界说Hook(这里是useCurrency)。

currencyContext.js


const CURRENCIES = {
  Euro: {
    code: "EUR",
    label: "Euro",
    conversionRate: 1, // base conversion rate
  },
  Usd: {
    code: "USD",
    label: "US Dollar",
    conversionRate: 1.19,
  },
};
const useCurrency = () => {
  const [currency, setCurrency] = React.useContext(CurrencyContext);
  const handleCurrency = (value) => {
    setCurrency(value);
  };
  return { value: currency, onChange: handleCurrency };
};
const CurrencyProvider = ({ children }) => {
  const [currency, setCurrency] = React.useState(CURRENCIES.Euro);
  return (
    <CurrencyContext.Provider value={[currency, setCurrency]}>
      {children}
    </CurrencyContext.Provider>
  );
};
export { CurrencyProvider, useCurrency, CURRENCIES };

App.js

import { CurrencyProvider, useCurrency, CURRENCIES } from "./currency-context";
const App = () => {
  return (
    <CurrencyProvider>
      <CurrencyButtons />
    </CurrencyProvider>
  );
};
const CurrencyButtons = () => {
  const { onChange } = useCurrency();
  return Object.values(CURRENCIES).map((item) => (
    <CurrencyButton key={item.label} onClick={() => onChange(item)}>
      {item.label}
    </CurrencyButton>
  ));
};

useCallback


根本介绍

useCallback 回来一个 memoried 回调函数,只要在依靠项改动的时分,才会更新这个回调函数。useCallback能够处理引证持平问题,防止子组件进行屡次不必要的从头烘托。


根本运用

处理引证持平问题 1: useCallback + updater function(用函数来更新状况)

import React, { useState, useCallback, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const Button = React.memo(({ handleClick }) => {
  const refCount = useRef(0);
  return (
    <button
      onClick={handleClick}
    >{`button render count ${refCount.current++}`}</button>
  );
});
function App() {
  const [isOn, setIsOn] = useState(false);
  const handleClick = useCallback(() => setIsOn((prevIsOn) => !prevIsOn), []);
  return (
    <div className="App">
      <h1>{isOn ? "On" : "Off"}</h1>
      <Button handleClick={handleClick} />
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

处理引证持平问题 2:useCallback + useReducer(dispatch 函数作为依靠)

const Button = React.memo(({ handleClick, text }) => {
  const refCount = useRef(0);
  return (
    <button onClick={handleClick}>
      {`${text}`}
      <span className={"renderCount"}>
        self render count {refCount.current++}
      </span>
    </button>
  );
});
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREASE_A":
      return {
        ...state,
        numA: state.numA + 1,
      };
    case "DECREASE_A":
      return {
        ...state,
        numA: state.numA - 1,
      };
    case "INCREASE_B":
      return {
        ...state,
        numB: state.numB + 1,
      };
    case "DECREASE_B":
      return {
        ...state,
        numB: state.numB - 1,
      };
    case "A_PLUS_B":
      return {
        ...state,
        result: state.numA + state.numB,
      };
    case "A_MINUS_B":
      return {
        ...state,
        result: state.numA - state.numB,
      };
    default:
      return state;
  }
};
function App() {
  const [{ numA, numB, result }, dispatch] = useReducer(reducer, {
    numA: 0,
    numB: 0,
    result: null,
  });
  const handlePlusAClick = useCallback(
    () => dispatch({ type: "INCREASE_A" }),
    [dispatch]
  );
  const handleMinusAClick = useCallback(
    () => dispatch({ type: "DECREASE_A" }),
    [dispatch]
  );
  const handlePlusBClick = useCallback(
    () => dispatch({ type: "INCREASE_B" }),
    [dispatch]
  );
  const handleMinusBClick = useCallback(
    () => dispatch({ type: "DECREASE_B" }),
    [dispatch]
  );
  const handleAPlusB = useCallback(
    () => dispatch({ type: "A_PLUS_B" }),
    [dispatch]
  );
  const handleAMinusB = useCallback(
    () => dispatch({ type: "A_MINUS_B" }),
    [dispatch]
  );
  return (
    <div className="App">
      <div className={"num"}>NumA: {numA}</div>
      <Button text={"+"} handleClick={handlePlusAClick} />
      <Button text={"-"} handleClick={handleMinusAClick} />
      <div className={"num"}>NumB: {numB}</div>
      <Button text={"+"} handleClick={handlePlusBClick} />
      <Button text={"-"} handleClick={handleMinusBClick} />
      <div className={"num"}>Result: {result}</div>
      <Button text={"A + B"} handleClick={handleAPlusB} />
      <Button text={"A - B"} handleClick={handleAMinusB} />
    </div>
  );
}

处理引证持平问题 3:useCallback + useEffect(向 useEffect 传递引证持平的函数依靠)

function Foo({ bar, baz }) {
  React.useEffect(() => {
    const options = { bar, baz };
    buzz(options);
  }, [bar, baz]);
  return <div>foobar</div>;
}
function Blub() {
  const bar = React.useCallback(() => {}, []);
  const baz = React.useMemo(() => [1, 2, 3], []);
  return <Foo bar={bar} baz={baz} />;
}

useCallback 误区运用:给所有函数包装上 useCallback

import { useState, useCallback } from "react";
export const HookUsecallback = () => {
  const [count, setCount] = useState(0);
  const handleAddFn = useCallback(() => {
    console.log("handleAdd");
    setCount(count + 1);
  }, [count]);
  return (
    <>
      <div>{count}</div>
      <button onClick={() => handleAddFn()}>+1</button>
    </>
  );
};

上面的实例,就算不包裹 useCallback 也能够到达相同的作用,但useCallback 做了更多的作业,调用 React.useCallback,界说了一个数组 [],反而使得功能和内存变得糟糕。


useCallback 误区运用:运用 useCallback 来优化核算开支

  • 尽管能够在useCallback中的 memoried 函数傍边书写核算逻辑,每次调用也都会取得核算成果,可是 useCallback 回来的是一个函数,所以每次运用的时分,都会调用 memoried 函数从头核算。所以不能像 useMemo 相同回来一个引证持平的值来优化核算开支。

useMemo


根本介绍

useMemo 回来一个 memoried 值,只要在依靠项改动的时分,才会从头核算memoried值。useMemo从两个方面进行功能优化, 既能够处理引证持平问题,防止子组件进行屡次不必要的从头烘托又能够优化核算开支,防止屡次不必要的从头核算。


根本运用

处理优化核算开支问题:useMemo

import "./styles.css";
import { useMemo, useState } from "react";
export default function App() {
  const [count, setCount] = useState(3);
  const [name, setName] = useState("Ryan");
  function computedExpensiveValue(count) {
    console.log("computed expensive value");
    let sum = 0;
    while (count > 0) {
      sum += count;
      count--;
    }
    return sum;
  }
  const result = useMemo(() => computedExpensiveValue(count), [count]);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <input
        value={count}
        onChange={(e) => setCount(Number(e.target.value))}
      ></input>
      <input value={name} onChange={(e) => setName(e.target.value)}></input>
      <h2>{result}</h2>
      <h3>{name}</h3>
    </div>
  );
}

处理引证持平问题 1: useMemo + updater function(用函数来更新状况)

const Button = React.memo(({ handleClick }) => {
  const refCount = useRef(0);
  return (
    <button
      onClick={handleClick}
    >{`button render count ${refCount.current++}`}</button>
  );
});
function App() {
  const [isOn, setIsOn] = useState(false);
  const handleClick = useMemo(() => () => setIsOn((prevIsOn) => !prevIsOn), []);
  return (
    <div className="App">
      <h1>{isOn ? "On" : "Off"}</h1>
      <Button handleClick={handleClick} />
    </div>
  );
}

处理引证持平问题 2:useMemo + useEffect(向 useEffect 传递引证持平的函数依靠)

function Foo({ bar, baz }) {
  React.useEffect(() => {
    const options = { bar, baz };
    buzz(options);
  }, [bar, baz]);
  return <div>foobar</div>;
}
function Blub() {
  const bar = React.useCallback(() => {}, []);
  const baz = React.useMemo(() => [1, 2, 3], []);
  return <Foo bar={bar} baz={baz} />;
}

处理引证持平问题 3:useMemo + useReducer(dispatch 函数作为依靠)

import React, { useReducer, useMemo, useRef } from "react";
import "./App.css";
const Button = React.memo(({ handleClick, text }) => {
  const refCount = useRef(0);
  return (
    <button onClick={handleClick}>
      {`${text}`}
      <span className={"renderCount"}>
        self render count {refCount.current++}
      </span>
    </button>
  );
});
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREASE_A":
      return {
        ...state,
        numA: state.numA + 1,
      };
    case "DECREASE_A":
      return {
        ...state,
        numA: state.numA - 1,
      };
    case "INCREASE_B":
      return {
        ...state,
        numB: state.numB + 1,
      };
    case "DECREASE_B":
      return {
        ...state,
        numB: state.numB - 1,
      };
    case "A_PLUS_B":
      return {
        ...state,
        result: state.numA + state.numB,
      };
    case "A_MINUS_B":
      return {
        ...state,
        result: state.numA - state.numB,
      };
    default:
      return state;
  }
};
function App() {
  const [{ numA, numB, result }, dispatch] = useReducer(reducer, {
    numA: 0,
    numB: 0,
    result: null,
  });
  const handlePlusAClick = useMemo(
    () => () => dispatch({ type: "INCREASE_A" }),
    [dispatch]
  );
  const handleMinusAClick = useMemo(
    () => () => dispatch({ type: "DECREASE_A" }),
    [dispatch]
  );
  const handlePlusBClick = useMemo(
    () => () => dispatch({ type: "INCREASE_B" }),
    [dispatch]
  );
  const handleMinusBClick = useMemo(
    () => () => dispatch({ type: "DECREASE_B" }),
    [dispatch]
  );
  const handleAPlusB = useMemo(
    () => () => dispatch({ type: "A_PLUS_B" }),
    [dispatch]
  );
  const handleAMinusB = useMemo(
    () => () => dispatch({ type: "A_MINUS_B" }),
    [dispatch]
  );
  return (
    <div className="App">
      <div className={"num"}>NumA: {numA}</div>
      <Button text={"+"} handleClick={handlePlusAClick} />
      <Button text={"-"} handleClick={handleMinusAClick} />
      <div className={"num"}>NumB: {numB}</div>
      <Button text={"+"} handleClick={handlePlusBClick} />
      <Button text={"-"} handleClick={handleMinusBClick} />
      <div className={"num"}>Result: {result}</div>
      <Button text={"A + B"} handleClick={handleAPlusB} />
      <Button text={"A - B"} handleClick={handleAMinusB} />
    </div>
  );
}
export default App;

结合 useCallback.md,会发现 useCallback 能做的,useMemo 也能做,因为 useCallback 只能回来 memoried 函数,而 useMemo 既能回来 memoried 函数,也能回来 memoried 值。

useRef

根本介绍

useRef 主要用在三个方面,第一个方面是作为一个可变的实例值,该实例值的改动不会引起组件的从头烘托。一般用来盯梢不应该触发组件从头烘托的组件状况.第二个方面是 经过 ref 值引证 DOM,来改动 DOM 的状况(表单 focus blur ,按钮 disabled, HTML 元素 class)。第三个方面是经过 ref 回调函数来引证 DOM,将 DOM 节点作为参数传入.每次烘托都会调用该回调函数.


根本运用

useRef 作为可变的实例值:

事例 1: 判断组件是初次烘托仍是从头烘托
function ComponentWithRefInstanceVariable() {
  const [count, setCount] = React.useState(0);
  function onClick() {
    setCount(count + 1);
  }
  const isFirstRender = React.useRef(true);
  React.useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
    } else {
      console.log(
        `
          I am a useEffect hook's logic
          which runs for a component's
          re-render.
        `
      );
    }
  });
  return (
    <div>
      <p>{count}</p>
      <button type="button" onClick={onClick}>
        Increase
      </button>
    </div>
  );
}

useRef ref 值作为 DOM 引证

事例 1:运用 ref focus input
function App() {
  return <ComponentWithDomApi label="Label" value="Value" isFocus />;
}
function ComponentWithDomApi({ label, value, isFocus }) {
  const ref = React.useRef(); // (1)
  React.useEffect(() => {
    if (isFocus) {
      ref.current.focus(); // (3)
    }
  }, [isFocus]);
  return (
    <label>
      {/* (2) */}
      {label}: <input type="text" value={value} ref={ref} />
    </label>
  );
}
事例 2: 运用 ref 改动文档标题
function ComponentWithRefRead() {
  const [text, setText] = React.useState("Some text ...");
  function handleOnChange(event) {
    setText(event.target.value);
  }
  const ref = React.useRef();
  React.useEffect(() => {
    const { width } = ref.current.getBoundingClientRect();
    document.title = `Width:${width}`;
  }, [text]);
  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

useRef ref 回调函数作为 DOM 引证

每次组件烘托的时分,都会调用 ref 回调函数,传入 DOM 节点作为参数. ref 回调函数对比 ref 值,摆脱了 useEffect 和 useRef 的运用.

事例 1: ref 回调函数作为 DOM 引证
function ComponentWithRefRead() {
  const [text, setText] = React.useState("Some text ...");
  function handleOnChange(event) {
    setText(event.target.value);
  }
  const ref = (node) => {
    if (!node) return;
    const { width } = node.getBoundingClientRect();
    document.title = `Width:${width}`;
  };
  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}
事例 2: useCallback 增强 ref 回调函数作为 DOM 引证

运用 useCallback (依靠为空数组) 能够包裹 ref 函数,只使得 ref 函数在第一次挂载后履行,之后组件更新,不会履行 ref 函数.来增强 ref 函数.

function ComponentWithRefRead() {
  const [text, setText] = React.useState("Some text ...");
  function handleOnChange(event) {
    setText(event.target.value);
  }
  const ref = React.useCallback((node) => {
    if (!node) return;
    const { width } = node.getBoundingClientRect();
    document.title = `Width:${width}`;
  }, []); // 加上依靠和不包裹useCallback是相同作用
  return (
    <div>
      <input type="text" value={text} onChange={handleOnChange} />
      <div>
        <span ref={ref}>{text}</span>
      </div>
    </div>
  );
}

useImperativeHandle

根本介绍

useImperativeHandle 能够让你在运用 ref 时自界说露出给父组件的实例值。

根本运用

import "./styles.css";
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
const InputRef = forwardRef((props, ref) => {
  const inputEl = useRef(null);
  // useImperativeHandle 能够削减向父组件露出DOM节点的属性,只露出第二参数的目标,作为dom.current
  useImperativeHandle(ref, () => ({
    color: "green",
    value: inputEl.current.value,
  }));
  return <input ref={inputEl}></input>;
});
export default function App() {
  const inputRef = useRef(null);
  const [isShow, setShow] = useState(true);
  return (
    <div className="App">
      <button onClick={() => console.log(inputRef.current)}>获取</button>
      <h1>Hello CodeSandbox</h1>
      <button onClick={() => setShow(!isShow)}>切换</button>
      <InputRef ref={inputRef} />
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

React Hooks in TypeScript

useState

假如 useState 传入的初始值是简略值,类型会被自动揣度,假如是 null 或者是 undefined,或者是目标,数组等,能够运用传入泛型.

// inferred as number
const [value, setValue] = useState(0);
// explicitly setting the types
const [value, setValue] = (useState < number) | (undefined > undefined);
const [value, setValue] = useState < Array < number >> [];
interface MyObject {
  foo: string;
  bar?: number;
}
const [value, setValue] = useState < MyObject > { foo: "hello" };

useContext

useContext 能够根据传入的 Context 目标的类型进行揣度,不需求显现注册类型.

type Theme = "light" | "dark";
const ThemeContext = createContext < Theme > "dark";
const App = () => (
  <ThemeContext.Provider value="dark">
    <MyComponent />
  </ThemeContext.Provider>
);
const MyComponent = () => {
  const theme = useContext(ThemeContext);
  return <div>The theme is {theme}</div>;
};

useEffect / useLayoutEffect

因为 useEffect / useLayoutEffect 函数没有露出处理回来值的接口,所以不需求类型

useEffect(() => {
  const subscriber = subscribe(options);
  return () => {
    unsubscribe(subscriber)
  };
}, [options]);

useMemo / useCallback

useMemouseCallback 都能够根据回来值来揣度类型.

const value = 10;
// inferred as number
const result = useMemo(() => value * 2, [value]);
const multiplier = 2;
// inferred as (value: number) => number
const multiply = useCallback(
  (value: number) => value * multiplier,
  [multiplier]
);

useRef

null 作为初始值,泛型作为 ref 类型.

const MyInput = () => {
  const inputRef = useRef < HTMLInputElement > null;
  return <input ref={inputRef} />;
};

ref 用来保存可变的实例值时类型,ref.current 的类型 会被自动揣度。

const myNumberRef = useRef(0);
myNumberRef.current += 1;

useReducer

useReducer 会从 reducer 函数的参数中揣度出 要派发(dispatch)action 类型,以及 storestate 的类型。

interface State {
  value: number;
}
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "incrementAmount", amount: number };
const counterReducer = (state: State, action: Action) => {
  switch (action.type) {
    case "increment":
      return { value: state.value + 1 };
    case "decrement":
      return { value: state.value - 1 };
    case "incrementAmount":
      return { value: state.value + action.amount };
    default:
      throw new Error();
  }
};
const [state, dispatch] = useReducer(counterReducer, { value: 0 });
dispatch({ type: "increment" });
dispatch({ type: "decrement" });
dispatch({ type: "incrementAmount", amount: 10 });
// TypeScript compilation error
dispatch({ type: "invalidActionType" });

useImperativeHandle

MyInputHandles 为露出 ref 目标的类型,MyInputProps 是被转发组件 props 类型。

export interface MyInputHandles {
  focus(): void;
}
const MyInput: RefForwardingComponent<MyInputHandles, MyInputProps> = (
  props,
  ref
) => {
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => {
      if (inputRef.current) {
        inputRef.current.focus();
      }
    },
  }));
  return <input {...props} ref={inputRef} />;
};
export default forwardRef(MyInput);
import MyInput, { MyInputHandles } from "./MyInput";
const Autofocus = () => {
  const myInputRef = useRef<MyInputHandles>(null);
  useEffect(() => {
    if (myInputRef.current) {
      myInputRef.current.focus();
    }
  });
  return <MyInput ref={myInputRef} />;
};