本文主要记录常用 hooks 的用法和理解。
useState 让函数组件也可以有记住 state 的能力。
class 组件这么写:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "Hello",
};
}
}
函数组件中使用 useState
这么写:
const App = () => {
const [value, setValue] = useState("Hello");
const callback = () => {
// 调用后会导致重新渲染
setValue('World');
}
};
第二个参数 setValue
等于 Class 组件的 this.setState()
函数签名:
const [state, setState] = useState(initialState);
setState
说明setState 可以传入值,也可以传入函数 ,若传入函数,将会得到参数为当前 state 值。
举例:
const handleCountAddByCallBack = useCallback(() => {
setCount((count) => count + 1);
}, []);
useEffect(官方文档)主要用来解决组件一些副作用的问题。比如组件渲染后从服务端取回用户名回显;修改网页的 title;发起一个 WebSocket 的链接。
useEffect 相当于 class 组件的 componentDidMount
和 componentWillUnmount
的组合。
// setup 可以是一个异步函数
function useEffect(setup: (() => undefined|() => void) | () => Promise<void|(() => void)>; dependencies?: ReadonlyArray<unknown>);
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
setup
useEffect
接受一个函数,该函数立即执行,可以有副作用;此时相当于 在 componentDidMount
生命周期。cleanupFunction
,那么在组件卸载时将会执行该函数,你可以在cleanupFunction
中做一些清理工作。此时相当于 在 componentWillUnmount
生命周期。dependencies
如果不希望 useEffect 每次渲染都执行,这时可以使用它的第二个参数 dependencies
,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。
如下代码示例,传入 [serverUrl, roomId]
后只有在 serverUrl
roomId
变化时才会再次执行 setup
函数。
import { useEffect } from "react";
import { createConnection } from "./chat.js";
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
useEffect 做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect?
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect 会在每次渲染后都执行吗?
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
import { useReducer } from "react";
const initialState = { pageNum: 1, pageSize: 15 };
const reducer = (state, action) => {
switch (action.type) {
case "next": // 下一页
return { ...state, pageNum: state.pageNum + 1 };
case "prev": // 前一页
return { ...state, pageNum: state.pageNum - 1 };
case "changePage": // 跳转到某页
return { ...state, pageNum: action.payload };
case "changeSize": // 更改每页展示条目数
return { pageNum: 1, pageSize: action.payload };
default:
return state;
}
};
const Page = () => {
const [pager, dispatch] = useReducer(reducer, initialState);
return (
<Table
pageNum={pager.pageNum}
pageSize={pager.pageSize}
onGoNext={() => dispatch({ type: "next" })}
onGoPrev={() => dispatch({ type: "prev" })}
onPageNumChange={(num) => dispatch({ type: "changePage", payload: num })}
onPageSizeChange={(size) =>
dispatch({ type: "changeSize", payload: size })
}
/>
);
};
const value = useContext(MyContext);
import React, { createContext, useContext } from "react";
const ThemeContext = createContext({ theme: "light" });
function Button() {
const { theme } = useContext(ThemeContext);
return <div>current theme is {theme}</div>;
}
一起使用:
import { createContext, useReducer, useContext } from "react";
const ParentDispatch = createContext(null);
const Parent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ParentDispatch.Provider value={dispatch}>
<DeepTree parentState={state} />
</ParentDispatch.Provider>
);
};
// 深层子组件
const DeepChild = () => {
const dispatch = useContext(ParentDispatch);
const handleClick = () => {
dispatch({ type: "add", payload: "hello" });
};
return <button onClick={handleClick}>Add</button>;
};
type DependencyList = ReadonlyArray<any>;
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
type DependencyList = ReadonlyArray<any>;
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
一句话: useCallback 缓存钩子函数,useMemo 缓存返回值(计算结果)
对比:
无 | useContext | useReducer |
---|---|---|
返回值 | 一个缓存的回调函数 | 一个缓存的值 |
参数 | 需要缓存的函数,依赖项 | 需要缓存的值(也可以是个计算然后再返回值的函数) ,依赖项 |
使用场景 | 父组件更新时,通过 props 传递给子组件的函数也会重新创建,然后这个时候使用 useCallBack 就可以缓存函数不使它重新创建 | 组件更新时,一些计算量很大的值也有可能被重新计算,这个时候就可以使用 useMemo 直接使用上一次缓存的值 |
未使用 hooks
import React, { FC, useCallback, useMemo, useState } from 'react';
const Index: FC = (props) => {
const [count, setCount] = useState(0);
const isEvenNumber = count % 2 === 0;
const onClick = () => setCount(count + 1);
return (
<div>
<div>{count} is {isEvenNumber ? 'even':'odd'} number</div>
<button onClick={onClick}></button>
</div>
);
};
使用 hooks
import React, { FC, useCallback, useMemo, useState } from 'react';
const Index: FC = (props) => {
const [count, setCount] = useState(0);
const isEvenNumber = useMemo(() => {
return count % 2 === 0;
}, [count]);
const onClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<div>{count} is {isEvenNumber ? 'even':'odd'} number</div>
<button onClick={onClick}></button>
</div>
);
};
React 的函数组件是非常好用的东西,相比 class 写法以及 Vue 的对象挂载写法简洁很多,代码测试复用成本低,容易入手,但也带来一些问题,无状态函数很理想,但现实有一些计算开销大、组件渲染频繁的场景是需要状态的,每次都计算一遍状态(callback 和 二次计算值)无疑很浪费内存,函数不像对象(React class 写法或者 Vue 组件写法)可以直接将状态挂载在自身,没有浪费内存的问题,要实现类似的效果只能找一个的内存挂载点挂载这些东东,所以有了 useCallback 和 useMemo 这些 hook。
基于React提供的内置 Hooks 开发即可。本质上是函数的嵌套。
// 定义
export const useDialog = () => {
const [visible, setVisible] = useState(false);
const show = () => setVisible(true);
const hide = () => setVisible(false);
return [visible, show, hide];
}
// 使用
function Staff() {
const [visible, show, hide] = useDialog();
// do staff ...
}