你还是在用Redux来管理组件状态吗?

2,713 阅读5分钟

前言

为什么不用Redux

一句话概括就是使用Redux太麻烦了:

  • 比较适合中大型项目
  • 添加一次全局状态的过程及其麻烦且容易出错
  • Redux状态树变多时,维护更加困难

React Hook了解一下

自从React发布了React Hook后,大家就想到了使用useContextuseReducer来模拟Redux

好处是什么:

  • 可以和Redux分离使用,只需在用到的地方添加状态,相当于一个局部的全局状态。
  • 适合小型项目或个人学习React时开发
  • 不用写过多的代码,易于后续维护

先分别介绍一下useContextuseReducer分别是做什么的:

useContext

要使用useContext之前,我们需要先知道Context是什么,官方文档里面有说明

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

也就是说,使用Context来管理我们的“全局”状态是再好不过的了。

useContext就是为了接收context对象并返回context的值而存在的。说白了就是我们可以通过useContext来访问全局状态Context

useReducer

我们先来看一下useReducer的函数。

const [state, dispatch] = useReducer(reducer, initialArg, init);

它接收一个如(state, action) => newStatereducer,并返回当前的state以及预期配套的dispath方法。(用过Redux 应该不会陌生)官方文档

开始实践

下面我们用一个简单的例子通过useContextuseReducer来实现Redux的效果。

该例子使用实现主题色切换来管理全局状态

初始化项目

你可以通过create-react-app创建一个React项目进行测试,也可以在CodeSandbox进行模拟

笔者用的是TypeScript进行项目编写的,你也可以通过

npx create-react-app my-app(项目名) --typescript
# 或者
yarn create react-app my-app(项目名) --typescript

创建一个TypeScriptReact项目

创建两个子组件

创建好项目后,在src目录下新建一个pages

pages内分别创建switchtheme两个组件,这里只列出主要目录结构

.
├── src
|   └── pages
|   |   ├── switch
|   |   |   └── index.tsx   // 用来修改theme状态
|   |   └── state
|   |   |   └── index.tsx   // 用来显示theme状态
└── store
    └── theme.tsx           // 用来存储theme状态

创建State组件(用来显示theme状态)

index.tsx

import React from 'react'

const State: React.FC = () => {
    return (
        <div>由我显示全局变量</div>
    )
}

export default State;

创建Switch组件(用来修改theme状态)

index.tsx

import React from 'react'

const Switch: React.FC = () => {
    return (
        <section>
            <button>我是改变状态1</button>
            <button>我是改变状态2</button>
        </section>
    )
}

export default Switch;

现在我们把这两个组件都引入到App.tsx

import React from 'react';
import Switch from './pages/switch'
import State from './pages/state'
import './App.css';

const App: React.FC = () => {
  return (
    <div className="App">
      <Switch></Switch>
      <State></State>
    </div>
  );
}

export default App;

此时我们运行我们的项目,可以看到页面大概是这样的

初始状态

此时我们第一步就算完成了

状态管理

src目录下创建一个store目录文件,新建一个文件,名为theme.tsx

theme.tsx

初始化主题值

import React, { createContext, Context } from 'react'
// 定义主题色的接口
interface ITheme {
    theme: string
}
// 初始化
export const initialTheme: ITheme = {
    theme: '等待改变主题'
}
// 创建一个Context实例
export const ThemeContext: Context<any> = createContext(initialTheme);

/**
 * 创建一个 Theme 组件
 * Theme 组件包裹的所有子组件都可以通过调用 ThemeContext 访问到 value
 */
export const Theme: React.FC = (props) => {
    return (
        <ThemeContext.Provider value={{state: initialTheme}}>
            {props.children}
        </ThemeContext.Provider>
    )
}

这里有个重点就是,当ThemeContext.Providervalue值发生变化时,它内部的所有子组件都会重新渲染

所以我们需要将Theme组件包裹在StateSwitch的外层

此时App.tsx变为

import React from 'react';
import Switch from './pages/switch'
import State from './pages/state'
import { Theme } from './store/theme'
import './App.css';

const App: React.FC = () => {
  return (
    <div className="App">
      <Theme>
        <Switch></Switch>
        <State></State>
      </Theme>
    </div>
  );
}

export default App;

此时我们第二步就算完成了

State读取状态(useContext)

现在我们可以在State组件内通过useContext来读取Theme的状态了

// 引入useContext
import React, { useContext } from 'react'
// 引入Theme的Context
import { ThemeContext } from '../../store/theme'

const State: React.FC = () => {
    // 获取Theme传来的value值
    const { state } = useContext(ThemeContext)

    return (
        <div className="theme light">{state.theme}</div>
    )
}

export default State;

此时运行我们的项目,发现State已经读取到了Theme的值,说明我们的第三步已经成功了

获取Theme状态成功

改变状态(useReducer)

接下来我们需要通过Switch内的两个按钮来改变Theme的值,看下State组件内的状态有没发生变化

在Theme内添加reducer

Theme组件内使用useReducer并且添加reducer方法用于修改theme的状态

import React, { createContext, Context, useReducer } from 'react'
// 定义主题色的接口
interface ITheme {
    theme: string
}
// 初始化
export const initialTheme: ITheme = {
    theme: '等待改变主题'
}
// 创建一个Context实例
export const ThemeContext: Context<any> = createContext(initialTheme);

// (新增)初始化store的类型、初始化值、reducer
export const CHANGE_THEME: string = 'CHANGE_THEME';

// (新增)编写reducer函数
export const reducer = (state: ITheme, action: any) => {
    switch (action.type) {
        case CHANGE_THEME:
            return { ...state, theme: action.theme }
        default:
            throw new Error();
    }
}

/**
 * 创建一个 Theme 组件
 * Theme 组件包裹的所有子组件都可以通过调用 ThemeContext 访问到 value
 */
export const Theme: React.FC = (props) => {
    // 通过使用useReducer更新状态
    const [state, dispatch] = useReducer(reducer, initialTheme);
    return (
        <ThemeContext.Provider value={{state, dispatch}}>
            {props.children}
        </ThemeContext.Provider>
    )
}

此时我们的Theme组件已全部编写完毕。

Switch添加事件

我们Switch也通过useContext来读取Theme的状态,然后在button内添加修改状态的dispatch,便可以修改Theme的状态了

修改后的Swtich组件

import React, { useContext } from 'react'
import { ThemeContext, CHANGE_THEME } from '../../store/theme'

const Switch: React.FC = () => {
    // 调用dispatch修改状态
    const { dispatch } = useContext(ThemeContext)

    return (
        <section>
            <button 
                onClick={() => {
                    dispatch({ type: CHANGE_THEME, theme: "Change One" });
                }}>
                我是改变状态1
            </button>
            <button
                onClick={() => {
                    dispatch({ type: CHANGE_THEME, theme: "Change Two" });
                }}>
                我是改变状态2
            </button>
        </section>
    )
}

export default Switch;

运行项目效果如下

改变状态

至此,所有步骤完成。

demo已传到了GitHub,可以和你的代码进行对比😜

结尾

写这篇文章的初衷就是觉得在React中使用一次Redux真的太麻烦了,有了React Hook后的确少了不少重复的操作,希望能分享给大家。记得给我点个赞喔,算是对我一种鼓励吧,哈哈!

其他文章

如何根据背景颜色动态修改文字颜色