React 数据流向演进:从单向数据流到服务器端状态驱动应用的架构思考

React 数据流向演进:从单向数据流到服务器端状态驱动应用的架构思考

大家好,我是你们的老朋友,一个在 React 代码里摸爬滚打多年的资深工程师。

今天我们不聊怎么写 map 函数,也不聊怎么把 CSS 写成 tailwind。我们聊聊那个最核心、最让人头秃、也是最迷人的话题——数据是怎么流动的

如果把 React 比作一个烹饪大师,那数据流就是他的菜谱。如果菜谱乱了,做出来的菜能好吃吗?肯定不能,那基本就是黑暗料理。

今天,我们就来扒一扒 React 数据流向的进化史。从那个混乱的 jQuery 时代,到如今服务器端状态驱动应用的架构,这中间发生了很多故事,也踩了很多坑。准备好你们的咖啡,我们开始吧。


第一章:混乱的过去(jQuery 时代的“大杂烩”)

在 React 出现之前,前端世界是什么样子的?那是 DOM 操作的狂欢节,也是全局变量的大杂烩。

那时候,我们的代码长这样:

// jQuery 风格的“直接操作”
function initPage() {
    // 获取 DOM,像在菜市场抢白菜一样
    const userButton = $('#user-btn');
    const userName = $('#user-name');

    // 绑定事件
    userButton.on('click', () => {
        // 直接去服务器捞数据
        $.get('/api/user', (data) => {
            // 然后暴力修改 DOM
            userName.text(data.name);
            renderAvatar(data.avatar);
        });
    });
}

这有什么问题?

  1. 数据散落各处:数据(比如 data)就在回调函数里,和 UI 逻辑混在一起,想复用?难如登天。
  2. 没有“真相来源”:如果页面上有 10 个地方都显示了“用户名”,改一个地方,忘了改另一个,那就是灾难。
  3. 状态同步地狱:点击按钮,数据来了,然后呢?怎么通知页面其他部分更新?没人告诉你。

那时候的程序员,就像是一个没有导航的司机,开着一辆没有刹车的车,在 DOM 的泥潭里飞驰。虽然快,但随时可能翻车。


第二章:React 的诞生与“单向数据流”的契约

React 0.12 发布了,带来了一个新概念:单向数据流

这听起来像是一个无聊的合同,但它其实是给混乱世界立下的“宪法”。

核心原则:

  1. Props 向下流动:数据像接力赛一样,从父组件传给子组件。
  2. State 向上流动:用户操作(点击、输入)产生 State,然后更新 State,再通过 Props 传给子组件更新 UI。

最简单的例子:

// 父组件:拥有 State
function Counter() {
    const [count, setCount] = React.useState(0);

    // 点击时更新 State
    const handleClick = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <h1>当前计数: {count}</h1>
            {/* 把更新函数作为 Props 传下去 */}
            <Button onClick={handleClick} />
        </div>
    );
}

// 子组件:只负责展示和接收 Props
function Button({ onClick }) {
    return <button onClick={onClick}>点击我</button>;
}

这很美,对吧?
就像交通规则:红灯停绿灯行,不能逆行。所有的状态变化都是可预测的。如果你想改变 UI,你只需要改变 State,然后 React 会自动帮你渲染。

但是,这个“宪法”也有它的漏洞。这就是我们要聊的下一个阶段。


第三章:Props Drilling(属性钻取)的地狱

想象一下,你的应用结构是这样的:GrandParent -> Parent -> Child -> GrandChild

GrandParent 里面有一个数据(比如 currentUser),但是 GrandChild 需要用这个数据来渲染一个头像。

在 React 的早期,你是这么干的:

// GrandParent
function App() {
    const user = { name: "React Master", avatar: "..." };
    return (
        <Layout user={user}>
            <Dashboard>
                <UserProfile>
                    {/* GrandChild 需要这个 user */}
                    <UserProfileCard user={user} /> 
                </UserProfile>
            </Dashboard>
        </Layout>
    );
}

这就是“Props Drilling”(属性钻取)地狱。
你就像是在玩一个俄罗斯套娃,最里面的娃娃想要一个苹果,你不得不把苹果从最外面的娃娃开始,一层一层地传进去。

如果中间多了两个组件,或者你想复用一下这个 UserProfileCard,你会发现你的组件树里塞满了你根本不需要的 user={user}。这就像是你给全家每人发了一双筷子,虽然都能吃饭,但谁吃饱了谁没吃饱,没人知道。

这时候,开发者开始疯狂寻找解决方案。Context API 应运而生。

Context API:稍微舒服了一点,但还是有点挤

// 使用 Context
const UserContext = React.createContext();

function App() {
    const user = { name: "React Master", avatar: "..." };

    return (
        <UserContext.Provider value={user}>
            <Layout>
                <Dashboard>
                    <UserProfile>
                        <UserProfileCard /> {/* 终于不用传 Props 了 */}
                    </UserProfile>
                </Dashboard>
            </Layout>
        </UserContext.Provider>
    );
}

// 子组件消费
function UserProfileCard() {
    const user = React.useContext(UserContext);
    return <img src={user.avatar} />;
}

虽然不用传 Props 了,但 Context 也有问题。它不适合存储经常变化的数据(比如购物车数量),也不容易做缓存。而且,它依然没有解决“服务器状态”的问题。


第四章:Redux 与“单一真相来源”的宗教

随着应用变大,大家发现,光靠 Props 和 Context 已经无法满足需求了。我们需要一个全局的状态管理器。Redux 就是在这个时候像神一样降临了。

Redux 的哲学很简单:“唯一真相来源”

所有的数据都存在一个 Store 里,组件只能通过 Action(动作)和 Reducer(计算函数)来修改数据。它像是一个严格的官僚机构,你想改数据?行,填个申请表(Action),经过审批(Reducer),然后才能改。

Redux 标准样板代码:

// 1. 定义 Action Types (有点像填表头)
const ADD_TODO = 'ADD_TODO';

// 2. 定义 Action Creator (填表的人)
function addTodo(text) {
  return {
    type: ADD_TODO,
    payload: text
  };
}

// 3. 定义 Reducer (审批官,决定怎么改数据)
function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    default:
      return state;
  }
}

// 4. 创建 Store (档案室)
const store = Redux.createStore(todos);

// 5. 订阅更新 (监工)
store.subscribe(() => console.log(store.getState()));

// 6. Dispatch (提交申请)
store.dispatch(addTodo('Learn Redux'));

Redux 好在哪里?

  1. 可预测:因为数据流是线性的,你可以轻松调试。
  2. 时间旅行调试:你可以回滚到过去的状态,看看数据是怎么变的。
  3. 解耦:组件和 Store 是分离的。

Redux 坏在哪里?

  1. 样板代码太多:为了写一个 dispatch,你得写 Action Type、Action Creator、Reducer,甚至还要写中间件。如果你只是想改个标题,你可能得写 50 行代码。
  2. 学习曲线陡峭:对于小项目来说,Redux 就是杀鸡用牛刀。

这时候,大家开始反思:我们真的需要这么多中间件吗?我们能不能直接把数据拿过来用?


第五章:服务器状态(Server State)的混乱

这是 React 进化史中最关键的一章。我们一直聊的是客户端状态(Client State):点击了按钮、输入了文字、选中了复选框。这些状态存在组件的 useState 里,或者 Redux 里。

但是,绝大多数应用的数据来自服务器。比如:

  • 用户列表
  • 商品详情
  • 评论数据

服务器状态(Server State) 和客户端状态有着本质的区别:

  1. 不可变且不可控:你不能直接修改服务器的数据,你得发请求。
  2. 异步:网络是有延迟的,数据可能过期。
  3. 缓存:你不想每次切换页面都重新请求同一个数据。
  4. 竞态条件:用户快速点击两次,可能发送两个请求,第二个请求回来时,第一个请求的数据已经过时了。

传统的处理方式:useEffect 搞定一切

很多初学者(甚至老手)会这么写:

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    let isMounted = true; // 防止内存泄漏的标志

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setUser(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (isMounted) {
          setError(err);
          setLoading(false);
        }
      });

    return () => {
      isMounted = false;
    };
  }, [userId]);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了</div>;

  return <div>{user.name}</div>;
}

这代码看着还行,但问题很多:

  1. 逻辑混乱:数据获取逻辑、加载状态、错误处理都写在组件里,组件变得臃肿不堪。
  2. 无法复用:如果你想在其他组件里获取同一个用户信息,你得复制粘贴这段逻辑。
  3. 缓存失效:如果你刷新页面,数据丢了,你必须重新请求。
  4. 后台刷新:如果你在浏览器标签页里切换出去再回来,useEffect 会重新运行,导致闪烁。

这就是“状态地狱”。客户端状态和服务器状态混杂在一起,就像把洋葱和榴莲放在同一个冰箱里,味道太乱了。


第六章:Server Components(服务端组件)的革命

React 18 和 19 引入了 Server Components(服务端组件)。这是 React 架构的一次“核爆级”变革。

Server Components 到底是啥?
简单说,它就是在服务器上运行,然后只把 HTML 发给浏览器的组件

// app/user/[id]/page.tsx (Next.js 示例)
async function UserProfile({ params }) {
  // 注意这个 async 关键字!
  // 这段代码是在 Node.js 服务器上运行的,不是在浏览器里!
  const res = await fetch(`https://api.example.com/users/${params.id}`);
  const user = await res.json();

  return (
    <div>
      <h1>{user.name}</h1>
      {/* 这里的 user 对象直接在 HTML 中,不需要通过 JSON 序列化 */}
      <p>{user.bio}</p>
    </div>
  );
}

这有什么好处?

  1. 零 JavaScript:如果这个页面只读数据不交互,浏览器里根本不需要 React!页面加载速度极快,就像传统的 PHP 或 JSP 页面一样。
  2. 无需序列化开销:在客户端组件中,数据必须通过 JSON.stringify 传给 React,再解析。在服务端组件中,对象可以直接在服务端和客户端之间传递(基于流传输),没有序列化损耗。
  3. 隐藏 API 密钥:你可以在服务端组件里直接调用数据库或第三方 API,然后把结果发给浏览器。前端永远看不到你的密钥。

Server Components 解决了什么问题?
它解决了“服务器状态”的传输问题。因为数据本来就是在服务端获取的,现在直接在服务端渲染,不需要再“传”给客户端了。

但是,Server Components 并不能解决所有问题。它不能处理交互。


第七章:TanStack Query(React Query)与“数据获取即 UI”

既然 Server Components 处理了静态数据的渲染,那么动态的、需要交互的服务器数据怎么办?

这时候,TanStack Query(以前叫 React Query)登场了。它被认为是目前处理服务器状态的最佳实践。

核心思想:
不要把数据获取逻辑放在组件里。把数据获取逻辑放在一个“数据库”里,组件只负责“读取”数据。

代码示例:

import { useQuery } from '@tanstack/react-query';

function UserList() {
  // useQuery 自动帮你处理 loading、error、cache
  const { data, isLoading, isError } = useQuery({
    queryKey: ['users'], // 数据的唯一标识
    queryFn: fetchUsers, // 获取数据的方法
  });

  if (isLoading) return <div>正在从服务器拉取数据...</div>;
  if (isError) return <div>哎呀,服务器挂了</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 定义获取方法(可以在组件外部)
async function fetchUsers() {
  const res = await fetch('https://api.example.com/users');
  return res.json();
}

TanStack Query 做了什么?

  1. 自动缓存:你第一次请求 users,数据被缓存了。当你切换页面再回来,它直接从缓存里拿,0 网络请求。
  2. 后台更新:如果你在页面 A 修改了数据,页面 B 会自动重新获取最新数据。
  3. 失效策略:你可以手动告诉 Query,当数据更新后,哪些缓存需要失效。
  4. 乐观更新:用户点击“点赞”,你先在 UI 上显示“已点赞”,然后发请求。如果成功,保留;失败,回滚。

架构图解:

graph TD
    A[Client Component] --> B{数据类型?}

    B -->|UI 状态 (点击/输入)| C[useState / Context]
    C -->|需要持久化/复杂逻辑| D[Redux / Zustand]

    B -->|服务器数据 (列表/详情)| E[TanStack Query]
    E -->|获取/缓存/同步| F[Server API]

    G[Server Component] -->|直接渲染 HTML| H[Browser]

    style C fill:#e1f5ff
    style D fill:#e1f5ff
    style E fill:#ffe1f5
    style G fill:#f5ffe1

看,这就清晰多了。


第八章:现代架构的终极形态

现在,一个成熟的 React 应用架构通常是这样的:

  1. Server Components(服务端):负责渲染页面骨架、获取静态数据、执行数据库查询。它们是“主角”,负责把最重的活儿在服务器干完。
  2. Client Components(客户端):负责处理交互、事件监听。它们是“配角”,负责响应鼠标点击和键盘输入。
  3. TanStack Query(状态管理器):负责管理所有从服务器获取的数据。它是数据的“管家”。
  4. Redux/Zustand(客户端状态):只用于管理极少数的、局部的、需要持久化或复杂逻辑的客户端状态(比如模态框的开关、当前选中的 Tab)。

一个综合示例:

// app/products/[id]/page.tsx (Server Component)
async function ProductPage({ params }) {
  // 1. 服务端直接获取数据,渲染 HTML
  const res = await fetch(`https://api.example.com/products/${params.id}`);
  const product = await res.json();

  return (
    // 2. 传给客户端组件
    <ProductDetails product={product} />
  );
}

// components/ProductDetails.tsx (Client Component)
'use client'; // 必须标记

import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export default function ProductDetails({ product }) {
  const [isLiked, setIsLiked] = useState(false);
  const queryClient = useQueryClient();

  const handleLike = () => {
    // 3. 更新客户端状态
    setIsLiked(!isLiked);

    // 4. 乐观更新:立即更新缓存,提升体验
    queryClient.setQueryData(['product', product.id], (oldData) => ({
      ...oldData,
      likes: isLiked ? oldData.likes - 1 : oldData.likes + 1
    }));

    // 5. 异步同步到服务器
    fetch(`/api/products/${product.id}/like`, { method: 'POST' });
  };

  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold">{product.name}</h1>
      <p className="text-gray-600">{product.description}</p>

      <div className="mt-4 flex items-center gap-2">
        <button 
          onClick={handleLike}
          className={`px-4 py-2 rounded ${isLiked ? 'bg-red-500' : 'bg-blue-500'}`}
        >
          {isLiked ? '❤️ 已赞' : '🤍 点赞'}
        </button>
        <span>{product.likes} 人点赞</span>
      </div>
    </div>
  );
}

在这个架构中:

  • product 数据是在服务端组件里获取的,所以它不需要序列化,加载极快。
  • isLiked 状态是在客户端组件里管理的,因为它需要响应用户的点击。
  • likes 的变化使用了 TanStack Query 的缓存机制,实现了乐观更新。

第九章:未来展望——数据流向的“回归自然”

回顾 React 数据流向的演进,我们可以看到一种回归的趋势。

  • 早期:数据在 DOM 里乱跑(混乱)。
  • 中期:数据在 Props 和 State 之间硬传(僵化)。
  • 中期:数据在 Redux Store 里流转(官僚)。
  • 现在:数据在服务器和客户端之间智能流动(自然)。

未来的架构可能会更加模糊这两者的界限。

  1. GraphQL:让客户端决定要什么数据,不再有“过度获取”或“获取不足”的问题,进一步简化数据流向。
  2. Server Actions:React 19 的 Server Actions 允许你在服务端直接处理表单提交,把数据从客户端传回服务端的过程变得极其简单。
  3. 边缘计算:数据流将不再局限于一个数据中心,而是流向全球边缘节点,实现真正的低延迟。

总结一下(不总结也能懂):

不要把服务器状态当成普通的变量。不要把数据获取逻辑塞进 useEffect 里。不要在组件树里钻来钻去传 Props。

  • 服务器数据 -> 用 Server Components 或 TanStack Query。
  • 客户端状态 -> 用 useState 或 Redux。
  • 数据流向 -> 单向,清晰,可预测。

这就是现代 React 架构的精髓。希望今天的讲座能帮你理清这些乱麻,让你的代码像流水一样顺畅,像数据一样听话。

好了,下课!如果有问题,欢迎在评论区(或者我们的代码审查群里)砸砖头。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注