在 Redux Toolkit 应用程序中分离逻辑

2025-06-04

在 Redux Toolkit 应用程序中分离逻辑

Redux Toolkit(下文简称 RTK)是对 Redux 生态系统的重大改进。RTK 改变了我们编写 Redux 逻辑的方式,并以精简 Redux 所需的所有样板代码而闻名。

这几天我一直在用这个库,感觉挺开心的,但最近我发现一个不太好的情况:我所有的 Redux 逻辑,包括异步 API 调用,都被打包到了一个slice文件里(稍后会详细介绍切片)。

尽管这是 RTK 建议我们构建切片的方式,但随着应用程序的增长,文件开始变得难以导航,并最终变得不美观。

免责声明

这篇文章并不是关于如何使用 RTK 或 Redux 的入门指南,但是,我已经尽力解释了 RTK 的细微差别。

对 React 状态管理稍有了解就足以让你从本文中获益。你也可以随时访问文档来扩展你的知识。

切片

对于初学者来说,“切片”这个词可能比较陌生,所以我先简单解释一下。在 RTK 中,切片是一个函数,用于保存最终传递给 Redux Store 的状态。在切片中,用于操作状态的 Reducer 函数被定义并导出,以便应用中的任何组件都可以访问。

一个切片包含以下数据:

  • 切片的名称 - 因此可以在 Redux 存储中引用
  • initialState减速器
  • 用于改变状态的 Reducer 函数
  • extraReducers负责响应外部请求的参数(如下所示fetchPosts


import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

const initialState = []

// async function
export const fetchPosts = createAsyncThunk(
  'counter/fetchPosts',
  async (amount) => {
    const response = await fetch('https://api.backend.com').then((res) => res.json())
    return response.data;
  }
);

// slice
export const postSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    addPost: (state, action) => {
      // some logic
    },
  },
})

export const { addPost } = postSlice.actions
export default postSlice.reducer


Enter fullscreen mode Exit fullscreen mode

切片的基本概述

简而言之,切片文件是 RTK 应用程序的核心。让我们继续创建一个包含 RTK 的新 React 应用程序,运行以下命令:



    npx create-react-app my-app --template redux


Enter fullscreen mode Exit fullscreen mode

在代码编辑器中打开您的应用程序时,您会注意到此模板的文件夹结构与 create-react-app 的文件夹结构略有不同。

不同之处在于app包含 Redux 存储的新文件夹和features包含应用程序所有功能的文件夹。

文件夹中的每个子文件夹features代表 RTK 应用程序中的特定功能,其中包含切片文件、使用切片的组件以及您可能在此处包含的任何其他文件,例如样式文件。

这个生成的模板还包含一个示例counter组件,旨在向您展示使用 RTK 设置功能性 Redux 存储的基础知识以及如何从组件向该存储分派操作。

运行npm start以预览该组件。

通过 RTK 构建应用程序的方式,每个功能都是完全独立的,因此可以轻松地在一个目录中找到新添加的功能。

问题

让我们检查一下counterSlice.js



import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';

const initialState = {
  value: 0,
  status: 'idle',
};
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount) => {
    const response = await fetchCount(amount);
    return response.data;
  }
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
  // The `extraReducers` field lets the slice handle actions defined elsewhere,
  // including actions generated by createAsyncThunk or in other slices.
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state) => state.counter.value;

export default counterSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

正如我之前提到的,你会注意到,处理计数器组件状态所需的所有逻辑都合并到这个文件中。使用createAsyncThunkcreateSlice函数和extraReducers属性进行的异步调用都已包含在内。

随着应用程序的增长,您将继续向后端 API 发出更多异步请求,进而必须处理该请求的所有可能状态,以确保不会发生任何意外情况破坏您的应用程序。

在 RTK 中,请求有三种可能的状态:

  • 待办的
  • 满足和
  • 拒绝

请记住,处理这些情况至少需要 3 行代码。因此,一个异步请求至少需要 9 行代码。

想象一下,当你有10多个异步请求时,浏览文件会有多困难。这简直是一场我根本不想经历的噩梦。

解决方案

提高切片文件可读性的最佳方法是将所有异步请求委托给单独的文件,然后将其导入切片文件以处理请求的每个状态。

我喜欢使用“thunk”作为后缀来命名此文件,就像切片文件使用“slice”作为后缀一样。

为了演示这一点,我在应用中添加了一个与GitHub API交互的新功能。以下是当前的结构

功能
|_counter
|_github
|_githubSlice.js
|_githubThunk.js

githubThunk.js



import { createAsyncThunk } from '@reduxjs/toolkit'

// API keys
let githubClientId = process.env.GITHUB_CLIENT_ID
let githubClientSecret = process.env.GITHUB_CLIENT_SECRET

export const searchUsers = createAsyncThunk(
  'github/searchUsers',
    const res = await fetch(`https://api.github.com/search/users?q=${text}&
      client_id=${githubClientId}&
      client_secret=${githubClientSecret}`).then((res) => res.json())
    return res.items
  }
)

export const getUser = createAsyncThunk('github/getUser', async (username) => {
  const res = await fetch(`https://api.github.com/users/${username}? 
      client_id=${githubClientId}&
      client-secret=${githubClientSecret}`).then((res) => res.json())
  return res
})

export const getUserRepos = createAsyncThunk(
  'github/getUserRepos',
  async (username) => {
    const res = await fetch(`https://api.github.com/users/${username}/repos?per_page=5&sort=created:asc&
    client_id=${githubClientId}&
    client-secret=${githubClientSecret}`).then((res) => res.json())
    return res
  }
)


Enter fullscreen mode Exit fullscreen mode

有关如何使用的更多信息createAsyncThunk,请参考文档

然后将这些异步请求导入到切片文件中,并在extraReducers

githubSlice.js



import { createSlice } from '@reduxjs/toolkit'
import { searchUsers, getUser, getUserRepos } from './githubThunk'

const initialState = {
  users: [],
  user: {},
  repos: [],
  loading: false,
}

export const githubSlice = createSlice({
  name: 'github',
  initialState,
  reducers: {
    clearUsers: (state) => {
      state.users = []
      state.loading = false
    },
  },
  extraReducers: {
    // searchUsers
    [searchUsers.pending]: (state) => {
      state.loading = true
    },
    [searchUsers.fulfilled]: (state, { payload }) => {
      state.users = payload
      state.loading = false
    },
    [searchUsers.rejected]: (state) => {
      state.loading = false
    },
    // getUser
    [getUser.pending]: (state) => {
      state.loading = true
    },
    [getUser.fulfilled]: (state, { payload }) => {
      state.user = payload
      state.loading = false
    },
    [getUser.rejected]: (state) => {
      state.loading = false
    },
    // getUserRepos
    [getUserRepos.pending]: (state) => {
      state.loading = true
    },
    [getUserRepos.fulfilled]: (state, { payload }) => {
      state.repos = payload
      state.loading = false
    },
    [getUserRepos.rejected]: (state) => {
      state.loading = false
    },
  },
})

export const { clearUsers } = githubSlice.actions
export default githubSlice.reducer


Enter fullscreen mode Exit fullscreen mode

我承认 extraReducers 属性看起来仍然有点笨重,但我们最好这样做。幸运的是,这与普通 Redux 应用程序中使用 action 和 reducer 文件夹分离逻辑的方式类似。

将 Slice 添加到商店

你创建的每个切片都必须添加到 Redux Store 中,这样你才能访问其内容。你可以通过将 github 切片添加到 来实现这一点App/store.js



import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
import githubReducer from './features/github/githubSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    github: githubReducer,
  },
})


Enter fullscreen mode Exit fullscreen mode

另一件需要考虑的事情是 extraReducers 如何处理请求。在示例切片文件中,counterSlice您会注意到使用了不同的语法来处理请求。

在中githubSlice,我使用了 map-object 符号来extraReducers处理我的请求,主要是因为这种方法看起来更整洁并且更容易编写。

处理请求的推荐方式是使用示例counterSlice.js文件中所示的构建器回调。推荐使用这种方法,因为它对 TypeScript 的支持更好(因此,即使 JavaScript 用户也能使用 IDE 自动补全功能)。这种构建器表示法也是将匹配器 Reducer 和默认情况 Reducer 添加到切片的唯一方法。

可变性和不变性

此时,您可能已经注意到 RTK 中修改状态的方式与普通 Redux 应用程序或 React 的 Context API 中的修改方式存在对比。

RTK 让您可以使用“变异”语法编写更简单的不可变更新逻辑。



// RTK
state.users = payload

// Redux
return {
  ...state,
  users: [...state.users, action.payload]
}


Enter fullscreen mode Exit fullscreen mode

RTK 不会改变状态,因为它内部使用了Immer 库来确保你的状态不会发生改变。Immer 会检测到“草稿状态”的更改,并根据你的更改生成一个全新的不可变状态。

这样,我们就可以避免传统的先复制状态,然后再修改该副本以添加新数据的方法。点击此处,了解更多关于使用 Immer 编写不可变代码的信息。

在组件中调度动作

借助两个重要的钩子;useSelectoruseDispatch来自另一个名为的库react-redux,您将能够从任何组件调度您在切片文件中创建的操作。

使用此命令安装 react-redux



npm i react-redux


Enter fullscreen mode Exit fullscreen mode

现在你可以利用useDispatch钩子将操作发送到商店

Search.js



import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { searchUsers } from '../../redux/features/github/githubThunk'

const Search = () => {
  const dispatch = useDispatch()

  const [text, setText] = useState('')

  const onSubmit = (e) => {
    e.preventDefault()
    if(text !== '') {
      dispatch(searchUsers(text))
      setText('')
    }
  }

  const onChange = (e) => setText(e.target.value)

  return (
    <div>
      <form className='form' onSubmit={onSubmit}>
        <input
          type='text'
          name='text'
          placeholder='Search Users...'
          value={text}
          onChange={onChange}
        />
        <input
          type='submit'
          value='Search'
        />
      </form>
    </div>
  )
}

export default Search


Enter fullscreen mode Exit fullscreen mode

当请求得到满足时,你的 Redux 存储将填充数据

结论

Redux Toolkit 无疑是一个非常棒的库。它采取的诸多措施以及其简洁易用的特性,充分体现了它对开发者体验的重视。我真诚地认为,Redux Toolkit 应该是唯一的编写方式。

RTK 并未止步于此。他们的团队更进一步,打造了 RTK Query,这是一个旨在简化 Redux 应用程序中缓存和获取数据的库。RTK 成为 Redux 编写的主流只是时间问题。

你对这种方法以及 RTK 总体感觉如何?我很乐意收到你的反馈!😄

文章来源:https://dev.to/chinwike/separating-logic-in-your-redux-toolkit-application-h7i
PREV
Tailwind CSS 静态导航栏,滚动时带有阴影,适用于 Vue 应用程序
NEXT
像我五岁一样解释 JavaScript Promises。