前言
如果你是在使用React >= 16.8
的版本,那么你可以使用hooks
在编写你的组件,它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
为什么要使用 Hooks
- 提供代码逻辑复用的另一种选择(自定义 Hook),其他选择Render Props、HOC、mixins(不推荐使用)。
- 没有了 class 复杂的生命周期,组件写起来更清爽。性能也更好。
- 官方推荐使用,后续官网应该会不断完善
Hooks
。
Hooks 使用指南
useState
通常我们在使用 class 组件时,会在constructor
中初始化state
。constructor
只会在组件实例化时执行一次。同样的,使用useState
可以实现上述的操作。
如下代码initialCount
,只会在组件初始化时赋值给count
,后续如果要修改count
,需要通过setCount
来进行修改,这和this.setState
的使用方式也是一样的
const [count, setCount] = useState(initialCount);
如果你在初始化useState
,需要进行计算,可以使用如下方式
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
注意
- 与 class 组件中的
setState
方法不同的是useState
不会自动合并更新对象。所以需要将一个完整的对象传递给useState
// class
this.state = {
name: "smw",
age: 18
};
this.setState({
age: 20
});
// hooks
const [state, setState] = useState({
name: "smw",
age: 18
});
setState(preState => {
return {
...preState,
age: 20
};
});
- 在调用
setState
时,会将新老设置的值进行 Object.is 比较算法 。所以需要使用展开运算符或者Object.assign
来声明新值。
// 错误写法
const [state, setState] = useState({
name: "smw",
age: 18
});
setState(preState => {
preState.age = 20;
return preState;
});
// 正确写法
setState(preState => {
return {
...preState,
age: 20
};
});
ps:但是上述的操作其实并不够优雅,并且当你可能只是需要修改数组中的某个值,甚至你操作的数据是Set
或者Map
类型的,那处理起来就更复杂了。所以可能你需要immerjs来帮助你。
useReducer
useState
的升级方案,如果你的useState
存在比较复杂的处理场景可以使用useReducer
来处理,其思想和使用方式类似Redux
中的reducer
。
以下是一个官方demo
,实现了一个计数器
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
ps:
useReducer
可以获取上一次的state
值- 可以直接向子组件传递
dispatch
,实现父子组件之前的通信。
useRef、useImperativeHandle
useRef
:在Function
组件中,获取子组件ref
useImperativeHandle
:在Function
组件中,定义可被父组件获取到的ref
useRef
无论是获取Function
组件、class
组件还是真实dom
,方法都是一样的
const Index = () => {
const fnChildEl = useRef(null);
const inputEl = useRef(null);
const classChildEl = useRef(null);
useEffect(() => {
console.log("fnChildEl", fnChildEl);
console.log("inputEl", inputEl);
console.log("classChildEl", classChildEl);
}, []);
return (
<div>
<input ref={inputEl} type="text" />
<FnChild ref={fnChildEl} />
<ClassChild ref={classChildEl} />
</div>
);
};
在Function
组件中,使用callback ref的方式使用子组件的ref
一般什么情况下会使用这种方式来调用ref
例如:子组件中需要暴露ref
,存在延迟加载的情况,就需要使用这种方式。useRef
会在组件初始化时就获取ref
,但是初始化时,可能子组件需要暴露ref
的节点还没有被渲染。具体 demo 可以查看:demo
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
useImperativeHandle
在Function
组件中,暴露ref
给父组件使用
由于Function
组件没有实例,所以要使用useImperativeHandle
,声明自身的ref
。声明了以后就可以使用useRef
获取到Function
子组件的ref
function FnChild(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} type="text" />;
}
FnChild = forwardRef(FnChild);
useEffect
useEffect
会使函数延迟执行,也就是说React
会等待浏览器完成画面渲染之后才会延迟调用 useEffect
执行的特点:
- 执行的时机在 dom 渲染之后
- 通过
requestIdleCallback
异步执行
具体使用方式
只在初始化时执行一次,通过第二个参数传入空数组实现
可以实现componentDidMount
的调用
useEffect(() => {
// code
}, []);
在初始化时执行一次,当props.test
发生变化时会继续触发
可以实现componentDidUpdate
的调用
如果要获取 pre 参数,可以参考实现 getDerivedStateFromProp 章节
useEffect(() => {
// code
}, [props.test]);
在useEffect
返回一个函数可以实现componentWillUnmount
useEffect(() => {
// code
return () => {
// code
};
}, []);
ps:
- 与
componentDidMount
、componentDidUpdate
不同的是,在浏览器完成布局与绘制之后,传给useEffect
的函数会延迟调用。想了解关于useEffect
的更多使用细节可查看官方文件说明 - 第二个参数,传入数组中的每一个值也会使用
Object.is
进行比较。如果返回true
则不会触发。以及后续要介绍的类似用法的hooks
也是这样。
useLayoutEffect
其使用方式与useEffect
相同,区别只是调用时机不同。
不同点是:
- 执行的时机在
dom
渲染之前 - 同步执行,会阻塞
dom
的渲染
官方示例:例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)
个人理解:如果dom
的渲染依赖某些初始化的计算,useLayoutEffect
可以规避掉,使用useEffect
异步计算然后更新dom
造成的页面抖动的情况。
useCallback
返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。
类似vue
中computed
属性的概念,只不过它是能够缓存某个方法,并且监听某些数据,如果这些数据没有改变,则useCallback
返回的总是那个被缓存的方法(并且缓存当时的作用域中的变量值),只有当监听的数据发生变化才会更新作用域中的变量,并返回一个新的函数。
下面看一个例子来理解一下
index.jsx
// index.jsx
import React, { useState, useCallback } from "react";
import Button from "./button";
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<div>
<div>
<Button type="1" onClickButton={handleClickButton1}>
Button1
</Button>
</div>
<div>
<Button type="2" onClickButton={handleClickButton2}>
Button2
</Button>
</div>
</div>
);
}
Button.jsx
// Button.jsx
import React from "react";
const map = {};
const Button = ({ onClickButton, type, children }) => {
console.log(type, map[type] === onClickButton);
map[type] = onClickButton;
return (
<>
<button onClick={onClickButton}>{children}</button>
<span>{Math.random()}</span>
</>
);
};
export default React.memo(Button);
// export default Button
第一种情况传入[count2]
效果是:
- 点击
handleClickButton1
时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1
发生了变化 - 点击
handleClickButton2
时,会触发 button1 和 button2 的 update,因为,count2 更新了,所以触发了handleClickButton2
生成了一个新的函数
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count1]);
第二种情况传入[]
效果是:
- 点击
handleClickButton1
时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1
发生了变化 - 点击
handleClickButton2
时,只有第一次点击时会触发 button2 的 update,[]
意思是只有第一次初始化时会生成一个handleClickButton2
,后续触发的一直是原来的那个handleClickButton2
,同时handleClickButton2
的作用域会被缓存起来,所以每次点击其实都是触发setCount2(0 + 1);
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, []);
第二种情况传入[count1]
效果是:
- 点击
handleClickButton1
时,只会更新触发 button1 的 update,因为,因为只有handleClickButton1
发生了变化 - 点击
handleClickButton2
时,第一次点击时会触发 button2 的 update,后续如果要触发 button2 的 update,就必须先点击handleClickButton1
,触发 count1 的变化。
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count1]);
useMemo
传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值。
看完了useCallback
,再来看useMemo
,就很好理解了,useMemo
会返回一个值,并监听某些值的变化,只有当监听的值发生变化,才会触发useMemo
的重新计算,其实它就是vue
中computed
这个api
的react
版本。
注意:
由于useMemo
还不是很稳定,后期可能会修改,所以官方建议如果某个值从不需要被重新计算,可以惰性初始化 一个值
- 初始化创建一个昂贵的
state
// bad
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用
const [rows, setRows] = useState(createRows(props.count));
// ...
}
// good
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
- 避免重新创建
useRef()
的初始值
// bad
function Image(props) {
// ⚠️ IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
// good
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
useContext
Function
组件实现context
跨层级组件通信的解决方案。
下面是一个官方的demo
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
同时Function
组件也可以这样使用context
,但是会增加组件层级的嵌套
import React from "react";
const Context = React.createContext();
const Provider = Context.Provider;
const Consumer = Context.Consumer;
const store = {
userInfo: {
userId: 1,
userName: "smw"
}
};
function MyContext(props) {
return (
<Provider value={store}>
<div>
Context
<OneLevel />
</div>
</Provider>
);
}
function OneLevel(props) {
return (
<div>
OneLevel
<Consumer>{ctx => <TwoLevel {...ctx} />}</Consumer>
</div>
);
}
Hooks 使用技巧
实现 getDerivedStateFromProps
在使用hooks
时,我们可以这样实现getDerivedStateFromProps
例如:当父组件传递过来的userId
发生变化时,我们需要重新获取userInfo
,来更新视图。
function UserInfo({ userId }) {
const [userInfo, setUserInfo] = useState(() => {
const userInfo = getUserInfoByUserId(userId);
setPrevUserId(userInfo);
});
const [prevUserId, setPrevUserId] = useState(userId);
if (userId !== prevUserId) {
// 获取新的userId,去获取数据
const userInfo = getUserInfoByUserId(userId);
setPrevUserId(userId);
}
return (
<div>
<p>userName:{userInfo.userName}</p>
</div>
);
}
以上是官方推荐的实现方法,但是你要单独创建一个state
用来保存preProps
,如果你只需要监听变化,而不要用到preProps
,个人认为以下方式实现起来更简单明了,useEffect
在初始化的时候会执行一次,同时当userId
发生变化也会触发。满足getDerivedStateFromProps
的触发规则。
const UserInfo = ({ userId }) => {
const [userInfo, setUserInfo] = useState({});
useEffect(() => {
const userInfo = userInfoMap[userId];
setUserInfo(userInfo);
}, [userId]);
return (
<div>
<p>name : {userInfo.userName}</p>
</div>
);
};
实现 shouldComponentUpdate
可以用 React.memo
包裹一个组件来对它的 props 进行浅比较
- 等效于
PureComponent
,但是只比较props
- 自定义
props
比较逻辑
const MemoChild = React.memo(Child, (prevProps, nextProps) => {
// 比较prevProps和nextProps
// 返回true则不更新
return true;
});
React.memo
不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用useMemo
优化每一个具体的子节点。
如何避免向下传递回调
如果一个子组件需要调用父父组件的方法,需要将这个方法使用props
一层层向下传递。写起来很麻烦。
React
官方推荐通过context
,用useReducer
往下传递一个dispatch
函数
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
欢迎关注微信公众号
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。