携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天,点击查看活动详情
大家好,我是爱吃鱼的桶哥Z。自从React V16.8版本增加了hooks后,我们不必在编写类型复杂的class组件即可操作组件的状态了,虽然hooks已经发布快三年了,但是现在依旧还有很多hooks值得我们继续深入学习和使用,今天就给大家带来几个我们常用的hooks。
状态管理
在我们日常的开发中,如果我们需要在多个组件中共用某些数据或状态,而这些页面的层级又没有关联性时,一般我们都需要借助第三方的库来实现,例如:Redux
、Mobx
、Recoil
等,但是我们有时候往往又不想为了几个页面而引入一个第三方的状态库,这时候我们的主角就可以登场了,它就是createContext
,其实这个API并不是React在16.8以后的版本添加的,在之前的版本中就已经有了,下面我们一起来学习一下这个API的具体使用方法。
React.createContext 这个 API 从名字就可以看出,createContext能够创建一个React
的上下文(context),然后在订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。具体的API文档在这里,它的基本使用方法如下:
// context.tsx
const defaultContext = null;
export const Context = React.createContext(defaultContext);
其中的defaultContext
是传入的默认值或初始值,如果我们在组件中需要使用创建的上下文,即在组件中使用公用的数据或状态,则需要在最外层组件上通过 Context.Provider
将组件包裹一层,并通过在组件上显示的添加 value={{xx: xx}}
的形式传入 value
,用于指定context要对子组件暴露的相关信息。
为什么需要设定默认值呢?
因为子组件在使用的过程中,如果匹配不到最新的 Provider
,则会使用默认值。默认值一般在设定某些固定值或用于测试组件数据的时候会比较有用,一般我们都不会给一个固定的默认值用于实际的开发。
通过 React.createContext
我们创建了一个上下文的状态管理,那么我们该如何在相关的子组件中来使用这些公共的数据或状态呢?
useContext
在上面的内容中,我们使用React.createContext
创建了React相关的上下文,在子组件中,我们需要通过 useContext
这个hooks
来获取到Provider
传递的相关内容,useContext
的文档可以看这里,它的基本使用方法如下:
import { context } from './context';
const { status } = useContext(Context);
通过上述的代码,我们可以发现useContext
的参数是context
这个Context实例,而不是字符串。之所以我们在上面要将context
导出,然后在这个文件中导入来使用,就是为了避免父子组件不在一个文件夹中而无法共用context
的情况。
我们的准备工作已经完成了,接下来我们使用 createContext
和 useContext
实现数据和方法的共享。举一个实际开发中的例子:在子组件中修改父组件的state。我们一般的做法是将父组件中的方法通过props
传递给子组件使用,如果子组件的层级较深,则需要一层一层的传递,并且如果某个子组件不需要使用到这个方法,它也需要往下进行透传,使用起来及其麻烦,而我们使用Context
的方式就能避免这样的问题发生。
首先我们需要在父组件中通过Provider
包裹一层,并且通过Provider
上的value
将方法提供给子组件,具体的代码如下所示:
//Parent.tsx
import React, { useState } from 'react';
import { Context } from './context';
import Child from './components/Child';
// 模拟异步请求
const requestData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 1000);
});
}
const ParentNode: React.FC = () => {
const [num, setNum] = useState<number>(0);
const [count, setCount] = useState<number>(0);
// 将父组件中的状态、方法通过Provider上的value方法传递下去
return (
<Context.Provider value={{setNum, setCount, requestData}}>
<Child num={num} count={count} />
</Context.Provider>
)
};
export default ParentNode;
接下来子组件通过useContext
来解析上下文,我们一起来看一下在子组件中该如何才能获取到父组件传递过来的方法。具体的代码如下:
//Child.tsx
import React, { useContext } from 'react';
import { Context } from './context';
interface ChildProps {
num: number;
count: number;
}
const Child: React.FC<ChildProps> = ({ num, count }) => {
const { setNum, setCount, requestData } = useContext(Context);
useEffect(() => {
requestData().then((res) => {
console.log(`Response Data: ${res}`);
});
}, []);
return (
<div>
<p>num is: {num}</p>
<p>count is: {count}</p>
<hr />
<div>
<button onClick={() => setNum(num + 1)}>num++</button>
<button onClick={() => setCount(count + 1)}>count++</button>
</div>
</div>
)
};
export default Child;
具体的执行效果,可以狠戳这里
上述的例子中,当我们在Child
组件中点击按钮时,直接调用了Parent
中通过Context
透传过来的方法,并且修改了父组件的状态,子组件自身则会重新渲染。这种方式显示的避免了props
需要层层传递的问题。虽然我们这里的子组件与父组件都在同一目录且只有一级子组件,但即使存在多级子组件的情况,也是可以直接修改父组件传过来的数据的。
useReducer
上面的例子中我们虽然实现了多级组件方法共用,但是却暴露出一个问题:所有的方法都需要显示的放在Context.Provider.value
中进行传递,这就会造成整个Context.Provider
上面的方法会越来越多,也显的越来越臃肿,针对这个问题,我们再次引入一个新的hook
,它就是useReduer
,API文档地址在这里。如果看过一点React
源码的童鞋,就会知道useState
内部其实也是基于useReducer
来进行数据的更新的,下面我们一起来看一下该如何改造上面的代码以解决我们提出的这个问题。
useReduer
基础使用方法如下:
const [state, dispatch] = useReducer(reducer, initialState);
其中reducer
是我们需要执行的规则,如果之前用过Redux
的童鞋,看到这里应该不会觉得陌生。如果这个地方看的不是很明白,可以自己动手写一下就大致清楚了。initialState
是初始化的数据,也就是一开始我们获取或指定的默认值。下面的父组件与之前相比只是去掉了setNum
和setCount
等设置state
的方法,并在Provider
的value
中只传入了dispatch
,具体可以看一下相关的代码修改。
//Parent.tsx
import React, { useReducer } from 'react';
import { Context } from './context';
import Child from './components/Child';
export enum Action {
NUM = "NUM",
COUNT = "COUNT"
}
export interface CountAction {
type: Action;
payload: number;
}
interface CountState {
count: number;
num: number;
}
const initialState = { num: 0, count: 0 };
const reducer = (state: CountState, action: CountAction) => {
const { type, payload } = action;
switch(type) {
case Action.NUM:
return {
...state,
num: state.num + payload
};
case Action.COUNT:
return {
...state,
count: state.count + payload
};
default:
return state;
}
};
const ParentNode: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ dispatch }}>
<Child num={state.num} count={state.count} />
</Context.Provider>
)
};
export default ParentNode;
上述父组件传递了dispatch
给子组件,因此子组件中只需要获取到dispatch
即可修改父组件中的state
,而子组件与之前相比,唯一修改的只是点击事件中不是执行setNum
和setCount
,而是执行dispatch
,具体代码如下:
import React, { useContext } from "react";
import { Context } from "../../context";
import { Action } from "../Parent";
interface ChildProps {
num: number;
count: number;
}
const Child: React.FC<ChildProps> = ({ num, count }) => {
const { dispatch } = useContext(Context);
return (
<div>
<p>num is: {num}</p>
<p>count is: {count}</p>
<hr />
<div>
<button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
<button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
</div>
</div>
)
};
export default Child;
具体的执行效果,可以狠戳这里
上述的代码修改中,我们解决了前面的问题,即在Context.Provider
中暴露方法过多的问题,但是我们子组件中的state
还是通过props
进行传递的,因此这里我们一样可以将state
也通过Context
传递给子组件,这样我们的子组件和父组件之间就不需要通过props
来取值了,大致的代码变化如下:
//Parent.tsx
...other code
const ParentNode: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
<Child />
</Context.Provider>
)
};
export default ParentNode;
子组件也需要跟着相应的改变:
//Child.tsx
...other code
const Child: React.FC = () => {
const { state, dispatch } = useContext(Context);
const { num, count } = state;
return (
<div>
<p>num is: {num}</p>
<p>count is: {count}</p>
<hr />
<div>
<button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
<button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
</div>
</div>
)
};
export default Child;
最终修改的效果可以狠戳这里
经过上述几个步骤的修改,我们的状态和操作方法都已经可以在所有的子组件中进行共用了,但是现在又产生了一个新的问题,即使有些子组件没有用到某个状态,但是经过Context
的透传,还是会引起子组件的渲染,这时候我们就需要告诉React
哪些组件在自身的值没有发生变化时不需要重复渲染。
一般我们能用到的方法有:memo
、useMemo
,但是当使用memo
的时候,子组件还是会触发重复渲染。这是因为memo
只会对props
进行浅比较,而通过Context
注入到子组件中的state
并不是通过props
来传递的,因此state
的变化必然会触发组件的重渲染,为此我们就需要用到useMemo
了。
其实在平时的面试中,如果问到关于React
相关的题目,一般也会问memo
和useMemo
的具体区别,那么你知道它们的区别吗?
useMemo
useMemo
官方的解释如下:把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
使用方法如下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
下面是对子组件进行修改,在组件内部我们使用useMemo
将组件进行一次包裹,并且声明所有的依赖性,这样只有当依赖性中的值发生改变,页面才会重新渲染,大致代码如下:
//Child.tsx
...other code
const Child: React.FC = () => {
const { state, dispatch } = useContext(Context);
return useMemo(() => {
const { num, count } = state;
return (
<div>
<p>num is: {num}</p>
<p>count is: {count}</p>
<hr />
<div>
<button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
<button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
</div>
</div>
)
}, [state, dispatch]);
};
export default Child;
优化后的代码可以狠戳这里
结尾
到这里,我们在这篇文章中使用到的hooks
就全部都介绍完毕了,虽然上面的demo写的比较简单,但在实际开发中还是可以很方便的帮助我们来处理数据、方法的共享,我们一起再来回顾一下用到的hooks
有哪些。
首先通过createContext
来创建一个Context
上下文,这个API
不是16.8版本以后才有的,在之前的版本中就已经存在;其次通过useContext
获取到Context
传递到组件中的state
和setNum
,方便我们对数据进行操作;然后通过useReducer
统一管理,方便在子组件中不用通过props
来获取数据和方法;最后通过useMemo
来优化组件,避免组件中state
没有发生变化时组件重复渲染的问题。
至此,我们的全部内容就结束了,当然对于React
的学习和使用我们还有很长的路要走,我们一起加油吧~
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家