我为什么停止使用 Redux
单页应用程序的问题
Redux 不是缓存
一种更简单的后端状态方法
Redux是 React 生态系统中的一项革命性技术。它使我们能够拥有一个包含不可变数据的全局存储,并解决了组件树中prop-drilling的问题。对于跨应用程序共享不可变数据而言,它仍然是一款出色的工具,并且具有极佳的扩展性。
但是我们为什么需要一个全局 store 呢?我们的前端应用真的那么复杂吗?还是我们想用 Redux 做太多事情了?
单页应用程序的问题
React 等单页应用程序 (SPA) 的出现,给我们开发 Web 应用的方式带来了诸多改变。将后端代码与前端代码分离,使我们能够专注于特定领域并分离关注点。然而,它也带来了诸多复杂性,尤其是状态相关的复杂性。
现在,异步获取数据意味着数据必须存储在两个地方:前端和后端。我们必须思考如何以最佳方式全局存储这些数据,以便所有组件都能访问,同时维护数据的缓存以减少网络延迟。现在,前端开发的很大一部分负担是如何维护全局存储,避免出现状态错误、数据非规范化和数据过时等问题。
Redux 不是缓存
我们大多数人在使用 Redux 和类似的状态管理库时遇到的主要问题是,我们将其视为后端状态的缓存。我们获取数据,使用 Reducer/Action 将其添加到 Store,然后定期重新获取以确保数据是最新的。我们让 Redux 承担了太多工作,并将其作为解决所有问题的万能方案。
需要记住的一件重要事情是,我们的前端和后端状态永远不会真正同步,充其量只能营造出一种它们同步的幻觉。这是客户端-服务器模型的缺点之一,也是我们首先需要缓存的原因。然而,缓存和维护同步状态极其复杂,因此我们不应该像Redux 鼓励的那样,从头开始重新创建后端状态。
当我们开始在前端重建数据库时,后端和前端职责之间的界限很快就变得模糊。作为前端开发者,我们不需要为了创建一个简单的 UI 而对表及其关系了如指掌。我们也不需要知道如何最好地规范化我们的数据。这项责任应该落在设计表的人——后端开发者身上。这样,后端开发者就可以以文档化的 API 形式为前端开发者提供一个抽象。
现在有无数的库(例如redux-observable、redux-saga和redux-thunk)围绕 Redux 构建,帮助我们管理后端数据,每个库都会给已经繁琐的库增加一层复杂性。我认为其中大多数都没有达到预期效果。有时,我们需要先退一步,再迈进一步。
如果我们不再尝试在前端代码中管理后端状态,而是将其视为只需定期更新的缓存,会怎么样?通过将前端视为从缓存中读取数据的简单显示层,我们的代码将变得非常易于使用,并且更易于纯前端开发人员访问。我们既可以获得关注点分离的所有好处,又避免了构建 SPA 的大部分弊端。
一种更简单的后端状态方法
我相信有几个库比使用 Redux(或类似的状态管理库)存储后端状态有了很大的改进。
反应查询
几个月来,我一直在大部分个人和工作项目中使用React Query
。它是一个库,API 非常简单,只包含几个钩子来管理查询(获取数据)和变更(更改数据)。 自从使用 React Query 以来,我不仅效率更高,而且比使用 Redux 时,样板代码的编写量减少了 10 倍。我发现现在更容易专注于前端应用程序的 UI/UX,而不必时刻关注整个后端状态。
为了将此库与 Redux 进行比较,可以查看代码中这两个方法的示例。我使用原生 JS、 React Hooks和axios实现了一个简单的 TODO 列表,并同时使用这两个方法从服务器获取数据。
首先,Redux 的实现:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import axios from 'axios';
const SET_TODOS = "SET_TODOS";
export const rootReducer = (state = { todos: [] }, action) => {
switch (action.type) {
case SET_TODOS:
return { ...state, todos: action.payload };
default:
return state;
}
};
export const App = () => {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
useEffect(() => {
const fetchPosts = async () => {
const { data } = await axios.get("/api/todos");
dispatch({
type: SET_TODOS,
payload: data}
);
};
fetchPosts();
}, []);
return (
<ul>{todos.length > 0 && todos.map((todo) => <li>{todo.text}</li>)}</ul>
);
};
请注意,这甚至还没有开始处理重新获取、缓存和失效。它只是加载数据并在加载时将其存储在全局存储中。
以下是使用 React Query 实现的相同示例:
import React from "react";
import { useQuery } from "react-query";
import axios from "axios";
const fetchTodos = () => {
const { data } = axios.get("/api/todos");
return data;
};
const App = () => {
const { data } = useQuery("todos", fetchTodos);
return data ? (
<ul>{data.length > 0 && data.map((todo) => <li>{todo.text}</li>)}</ul>
) : null;
};
默认情况下,此示例包含数据重新获取、缓存和过期失效功能,并提供了相当合理的默认值。您可以在全局级别设置缓存配置,然后就不用管它了——通常情况下,它会按照您的预期执行。有关其底层工作原理的更多信息,请参阅React Query 文档。您可以使用大量的配置选项,这只是冰山一角。
现在,只要您需要这些数据,就可以使用useQuery 钩子,并设置唯一键(在本例中为"todos"
),然后使用异步调用来获取数据。只要函数是异步的,具体实现方式就无关紧要——您可以轻松地使用Fetch API来代替 Axios。
为了改变我们的后端状态,React Query 提供了useMutation hook。
我还编写了一个精选的 React Query 资源列表,您可以在此处找到。
驻波比
SWR在概念上与 React Query 几乎完全相同。React Query 和 SWR 大约在同一时期开发,并且彼此之间相互影响。react -query 文档中也对这两个库进行了详尽的比较。
与 React Query 一样,SWR 也拥有非常易读的文档。在大多数情况下,这两个库都不会出错。无论未来哪种库会成为主流,从 SWR 进行重构都比 Redux 的混乱局面容易得多。
Apollo 客户端
SWR 和 React Query 专注于 REST API,但如果你需要类似 GraphQL 的东西,那么领先的竞争者是Apollo Client。你会很高兴地了解到它的语法几乎与 React Query 相同。
前端状态怎么样?
一旦你开始使用这些库之一,你就会发现在绝大多数项目中,Redux 显得有些矫枉过正。当你的应用程序的数据获取/缓存部分已经处理完毕后,前端需要处理的全局状态就非常少了。剩下的少量状态可以通过Context或useContext + useReducer来处理,从而创建你自己的伪 Redux。
或者更好的是,使用 React 的内置状态作为简单的前端状态。这样做本身并没有什么问题。
// clean, beautiful, and simple
const [state, setState] = useState();
让我们更全面地拥抱前后端分离,而不是停留在这种模棱两可的中间状态。这些新兴库代表了我们在单页应用中管理状态方式的转变,是朝着正确方向迈出的一大步。我非常期待看到它们引领 React 社区走向何方。
文章来源:https://dev.to/g_abud/why-i-quit-redux-1knl