你可能并不需要状态管理库

238 阅读4分钟

多数情况下,在前端项目里引入状态管理库是为了在组件树内共享一些状态,避免层层透传的 props 降低代码的可维护性。

但对于简单的项目,通过 React Context 来共享状态就已经能满足需求了。

为了方便介绍,假设现在需要用 Context 在组件树内共享值 sharedValue 和逻辑 addOne,要求:

  • 在 ComponentA 中展示 sharedValue 的值。
  • 在 ComponentB 中展示一个按钮,按钮点击后会调用 addOne 加一。

熟悉 React 的人可能会这样做

假设你已经熟悉 React 了,很轻松地就能写出这样的代码:

// xxContext.ts
import { createContext } from 'react';

export const Context = createContext();

// RootComponent.tsx
import { Context } from './xxContext.ts';
import { useState } from 'react'

export const RootComponent = () => {
	const [sharedValue, setSharedValue] = useState(1);
	const addOne = () => {
		setSharedValue(value => value + 1);
	}
	
	return <Context.Provider value={{ sharedValue, addOne }}>
		<ComponentA />
		<ComponentB />
	</Context.Provider>
}

// ComponentA.tsx
import { Context } from './xxContext.ts';
import { useContext } from 'react';

export const ComponentA = () => {
	const { sharedValue } = useContext(Context)
	return <div>
		当前 Context 中的值是 {sharedValue}
	</div>
}

// ComponentB.tsx
import { Context } from './xxContext.ts';
import { useContext } from 'react';

export const ComponentB = () => { 
	const { addOne } = useContext(Context);
	return <button onClick={addOne}>
		点我 +1
	</button>
};

但是这样做总感觉有点麻烦,因为在 ComponentA 和 ComponentB 中,我们既需要手动引入 Context,又需要通过 useContext 从 Context 上取值。

那能不能简化下呢?

把从 Context 上取值的逻辑抽成 hook

既然 React 引入 hook 的原因之一是为了逻辑复用,那我们就封装一个从 Context 上取值的 hook。

// xxContext.ts
import { createContext } from 'react';

export const Context = createContext();

export const useXXContext = () => useContext(Context); // ✅ 我们在 xxContext.ts 中新增这行代码

// RootComponent.tsx
import { Context } from './xxContext.ts';
import { useState } from 'react'

export const RootComponent = () => {
	const [sharedValue, setSharedValue] = useState(1);
	const addOne = () => {
		setSharedValue(value => value + 1);
	}
	
	return <Context.Provider value={{ sharedValue, addOne }}>
		<ComponentA />
		<ComponentB />
	</Context.Provider>
}

// ComponentA.tsx
import { useXXContext } from './xxContext.ts';

export const ComponentA = () => {
	const { sharedValue } = useXXContext() // ✅ 优雅地从 xxContext 上取值
	return <div>
		当前 Context 中的值是 {sharedValue}
	</div>
}

// ComponentB.tsx
import { useXXContext } from './xxContext.ts';

exort const ComponentB = () => { 
	const { addOne } = useXXContext();
	return <button onClick={addOne}>
		点我 +1
	</button>
};

但现在如果我们需要共享其他新的状态,就需要修改 RootComponent 的代码了。

有办法解耦开吗?

把需要被共享的状态抽成 hook

我们可以将 RootComponent 中需要共享的状态抽成 hook,放到单独的文件维护。

因为这个 hook 用于创建被共享的状态,我们可以起名为 useCreateXXContext

// xxContext.ts
import { createContext } from 'react';

export const Context = createContext();

export const useXXContext = () => useContext(Context);

// useCreateXXContext.ts 
import { useState } from 'react'

export const useCreateXXContext = () => { // ✅ 将被共享的状态抽成 hook
	const [sharedValue, setSharedValue] = useState(1);
	const addOne = () => {
		setSharedValue(value => value + 1);
	}
	
	return {
		addOne,
		sharedValue
	}
}

// RootComponent.tsx
import { Context } from './xxContext.ts';
import { useCreateXXContext } from './useCreateXXContext.ts';

export const RootComponent = () => {

	const context = useCreateXXContext() // ✅ 创建需要被共享的状态,并传递给 Context
	
	return <Context.Provider value={context}> 
		<ComponentA />
		<ComponentB />
	</Context.Provider>
}

// ComponentA.tsx
import { useXXContext } from './xxContext.ts';

export const ComponentA = () => {
	const { sharedValue } = useXXContext()
	return <div>
		当前 Context 中的值是 {sharedValue}
	</div>
}

// ComponentB.tsx
import { useXXContext } from './xxContext.ts';

exort const ComponentB = () => { 
	const { addOne } = useXXContext();
	return <button onClick={addOne}>
		点我 +1
	</button>
};

这样写好看多了,但现在我们还是需要编写一些样板代码,比如我们需要创建 Context、创建需要被共享的状态、将需要被共享的状态传到 Context 里,我们还需要创建一个从 Context 上取值的 hook。

有办法消灭这些样板代码吗?

用工厂模式消灭样板代码

我们先看一下 RootComponent.tsx,有两个地方不够”通用”,分别是调用 useCreateXXContext 和 Context.Provider 的 children

import { Context } from './xxContext.ts';
import { useCreateXXContext } from './useCreateXXContext.ts';

export const RootComponent = () => {

	const context = useCreateXXContext()
	
	return <Context.Provider value={context}> 
		<ComponentA />
		<ComponentB />
	</Context.Provider>
}

再来看一下 xxContext.tsx,这个文件内做的事情也很简单,也就是创建了一个 context,创建了一个从 context 上取值的 hook。

// xxContext.ts
import { createContext } from 'react';

export const Context = createContext();

export const useXXContext = () => useContext(Context);

那我们可以借助工厂模式:

  • 提取通用的部分:即创建 context、创建从 context 上取值的逻辑、将需要被共享的状态提供给 context
  • 通过参数抹平差异:即 RootComponent 所包裹的 children、业务需要共享的状态(在这里是 useCreateXXContext )
// create.ts
import { createContext, useContext } from 'react';

export const createModel = (useCreateContext) => { // ✅ useCreateContext 这个 hook 存放在了闭包中,只有在 Provider 在被渲染时,hook 才会真正的执行
	const context = createContext();
	
	const _useContext = () => useContext(context);
	
	const Provider = (props)=> { // ✅ 该组件的作用是将需要被共享的数据 提供 给子树
		const initialContext = useCreateContext()
		return <context.Provider value={initialContext}>
			{props.children}
		</context.Provider>
	}
	
	return {
		Provider,
		useContext: _useContext
	}
}

来试试你刚刚亲手封装好的“状态管理库”

至此,我们就基于 React Context 封装了一个最简单的状态管理库。它的 API 也非常简单,使用起来如下:

// xxModel.ts
const useCreateXXContext = () => { // ✅ 这里仍然是业务自定义的、需要被共享的状态
	const [sharedValue, setSharedValue] = useState(1);
	const addOne = () => {
		setSharedValue(value => value + 1);
	}
	
	return {
		addOne,
		sharedValue
	}
}

export default createModel(useCreateXXContext) // ✅ 将它传入给我们封装好的 createModel 方法

// RootComponent.tsx
import { Provider } from './xxModel.ts';

export const RootComponent = () => {	
  // ✅ 用 Provider 包裹需要被共享状态的子树
	return <Provider> 
		<ComponentA />
		<ComponentB />
	</Provider>
}

// ComponentA.tsx
import { useContext } from './xxModel.ts';

export const ComponentA = () => {
	const { sharedValue } = useContext()   // ✅ 直接从 Model 上引入共享的状态
	return <div>
		当前 Context 中的值是 {sharedValue}
	</div>
}

// ComponentB.tsx
import { useContext } from './xxModel.ts';

exort const ComponentB = () => { 
	const { addOne } = useContext();
	return <button onClick={addOne}>
		点我 +1
	</button>
}

创作不易

如果你觉得这篇文章对你有帮助的话,欢迎点赞、收藏、关注,你的支持将会是我更新的动力。