React - Hooks 知识体系概览
- 起步:我的Hooks 学习资料
- 开始:
Hooks产生的原因和产生背景 - Hooks Api:
- 状态钩子:
useState,useReducer - 副作用钩子:
useEffect,useLayoutEffect - 共享状态钩子:
useCotext - 记忆值钩子:
useMemo - 记忆回调函数钩子:
useCallback - ref 钩子:
useRef - ImperativeHandle钩子:
useImperativeHandle
- 状态钩子:
- React Hooks in TypeScript
- React Hooks 造轮子
- DOM 副作用修改 / 监听
- 组件辅助
- 动画
- 请求
- 表单
- 模拟生命周期
- 存数据
- 封装原有库
- React Hooks 源码:待续
起步
我的 Hook 学习资料
Hooks Start
useState - useReducer
Remove an Item from a List in React
How to update Item from a List in React
useEffect - useLayoutEffect
React useLayoutEffect vs. useEffect with examples
useContext
useMemo
【译】什么时候使用 useMemo 和 useCallback
useCallback
【译】什么时候使用 useMemo 和 useCallback
如何錯誤地使用 React hooks useCallback 來保存相同的 function instance
useRef
React: Using Refs with the useRef Hook
useImperativeHandle
React Hooks in TypeScript
React Hooks 造轮子
开始
Hooks 产生的原因,背景和目的
Hooks 产生的背景和目的
React Hooks 是在 2018 年 10 月的 React Conf 上引入的,替换原先类组件的书写方式,使得函数式组件支持 state 和 side-effects。React-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,以及更新state 的 setState 函数。
Tips:与
class组件当中setState不同,Hook的setState不会自动合并更新对象,所以我们会使用展开运算符,结合Hook的setState来达到更新对象的目的。
基本使用
- 传入初始状态,返回一个数组,数组的第一个成员是
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
基本介绍
我们说,useState 为 React-Hooks 引入了状态管理,useReducer 的作用也是为 React-Hooks 提供了状态管理。useReducer 是 useState 的增强和替代 API,useReducer 用于复杂状态管理和性能优化, 性能优化是因为它可以借助 Context API 传递 dispatch 函数给子组件,由于dispatch 函数始终不变,所以子组件不会重新渲染。
Tips:对于使用或者掌握了
Redux的开发者来讲,useReducer使用起来会很顺手。因为其中像:store,reducer,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
基本介绍
useEffect为React引入了副作用函数,你可以使用useEffect各种用法来决定,副作用函数在组件挂载时,组件渲染时还是在组件更新渲染时运行,你可以在副作用函数当中,进行mutations(状态改变),subscriptions(添加订阅),timers(设置定时器),logging(添加日志),fetch data(网络请求)。
每一次组件的渲染都有对应这一次渲染 state 和 props
在开始聊 useEffect 之前,我们先来聊一聊组件的渲染,我们首先要明确,每一次组件的渲染都有对应这一次 state 和 props,通过下面的定时器案例,你会对此有更深的了解。
// 这是一个简单的计数器案例
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 当中的 watcher,proxy,或者是 databinding。
每一次组件渲染,都有对应这一次渲染的事件处理函数
我们明确了每一次组件的渲染都有对应这一次 state 和 props之后,第二个要明确的是每一次组件渲染,都有对应这一次的事件处理函数。通过下面的案例,你会对此有更深的了解。
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 函数,等待每次 React 将 DOM 更新渲染绘制到 screen 上之后去调用 effects 函数,本质上,effects 函数每次“看到的”都是对应那一次渲染的 state 和 props.
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
基本介绍
useLayoutEffect 和 useEffect 有着一样的函数签名,两者在大多数情况下可以相互替换,useLayoutEffect 和 useEffect的本质区别是触发时机的不一致,useEffect 发生在将 DOM 渲染绘制到屏幕之后,而 useLayoutEffect 发生在 DOM 渲染绘制到屏幕之前同步触发.也就是useLayoutEffect比useEffect更快执行.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.Provider,context数据,改变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
useMemo 和 useCallback 都可以根据返回值来推断类型.
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 类型,以及 store 中 state 的类型。
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} />;
};