持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
前言
哈喽大家好,我是Lotzinfly,一位前端小猎人。欢迎大家再次来到前端丛林,在这里你将会遇到各种各样的前端猎物,我希望可以把这些前端猎物统统拿下,嚼碎了服用,并成为自己身上的骨肉。今天是国庆假期第七天,也是我们冒险的第七天,短暂的七天假期就这样结束了,不知道大家在这七天里有没有收获呢?
昨天我们介绍了useState这一钩子函数,今天我们会继续深入学习React高级用法,继续学习React的重点React Hooks。Hook 是 React 16.8 的新增特性,学会React Hooks将大大提高我们的开发效率,所以一定要掌握好React Hooks。在国庆假期的最后一天,让我们稳住继续学习,学会React实现弯道超车。你们准备好了吗?那么开始我们的冒险之旅吧!
1.useReducer
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
1.1 基本用法
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = 0;
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { number: state.number + 1 };
case 'decrement':
return { number: state.number - 1 };
default:
throw new Error();
}
}
function init(initialState) {
return { number: initialState };
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<>
Count: {state.number}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}
2.useContext
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
- 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
- useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者<MyContext.Consumer>
- useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
const CounterContext = React.createContext();
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { number: state.number + 1 };
case 'decrement':
return { number: state.number - 1 };
default:
throw new Error();
}
}
function Counter() {
let { state, dispatch } = useContext(CounterContext);
return (
<>
<p>{state.number}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
)
}
function App() {
const [state, dispatch] = useReducer(reducer, { number: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
<Counter />
</CounterContext.Provider>
)
}
3.useEffect
- 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI的一致性
- 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
- useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的componentDidMount 、 componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
- 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
useEffect(didUpdate);
3.1 通过class实现修标题
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0
};
}
componentDidMount() {
document.title = `你点击了${this.state.number}次`;
}
componentDidUpdate() {
document.title = `你点击了${this.state.number}次`;
}
render() {
return (
<div>
<p>{this.state.number}</p>
<button onClick={() => this.setState({ number: this.state.number + 1 })}>
+
</button>
</div>
);
}
}
在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行。
3.2 通过effect实现
import React, { Component, useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [number, setNumber] = useState(0);
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器的 API 更新页面标题
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={() => setNumber(number + 1)}>+</button>
</>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。
3.3 跳过 Effect 进行性能优化
- 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
- 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
function Counter() {
const [number, setNumber] = useState(0);
// 相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(() => {
setNumber(number => number + 1);
}, 1000);
}, []);
return (
<>
<p>{number}</p>
</>
)
}
3.4 清除副作用
- 副作用函数还可以通过返回一个函数来指定如何清除副作用
- 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个effect 之前,上一个 effect 就已被清除
import React, { useEffect, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [number, setNumber] = useState(0);
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(() => {
setNumber(number => number + 1);
}, 1000);
return () => {
console.log('销毁老的定时器');
clearInterval($timer);
}
});
return (
<>
<p>{number}</p>
</>
)
}
function App() {
let [visible, setVisible] = useState(true);
return (
<div>
{visible && <Counter />}
<button onClick={() => setVisible(false)}>stop</button>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
4.useRef
- useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
- 返回的 ref 对象在组件的整个生命周期内保持不变
const refContainer = useRef(initialValue);
4.1 useRef
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
let [number, setNumber] = useState(0);
return (
<>
<Child />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
</>
)
}
let input;
function Child() {
const inputRef = useRef();
console.log('input===inputRef', input === inputRef);
input = inputRef;
function getFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={getFocus}>获得焦点</button>
</>
)
}
ReactDOM.render(<Parent />, document.getElementById('root'));
4.2 forwardRef
- 将ref从父组件中转发到子组件中的dom元素上
- 子组件接受props和ref作为参数
function Child(props, ref) {
return (
<input type="text" ref={ref} />
)
}
Child = forwardRef(Child);
function Parent() {
let [number, setNumber] = useState(0);
const inputRef = useRef();
function getFocus() {
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef} />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
4.3 useImperativeHandle
- useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
- 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与forwardRef 一起使用
function Child(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => (
{
focus() {
inputRef.current.focus();
}
}
));
return (
<input type="text" ref={inputRef} />
)
}
Child = forwardRef(Child);
function Parent() {
let [number, setNumber] = useState(0);
const inputRef = useRef();
function getFocus() {
console.log(inputRef.current);
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef} />
<button onClick={() => setNumber({ number: number + 1 })}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
结尾
好啦,这期的前端丛林大冒险先到这里啦!这期我们介绍了React Hooks的useReducer、useContext、useEffect、useRef这几个钩子函数,相信让大家对React Hooks有了一定的了解,大家一定要好好啃下来嚼烂嚼透。希望大家可以好好品尝并消化,迅速升级,接下来我们才更好地过五关斩六将!好啦,我们下期再见。拜拜!