10 分钟内为您的 React 应用添加实时协作功能

有一天,我认识的一个人发来信息:

“假期期间,我发现自己试图通过 Zoom 向儿子解释投资回报是如何运作的,如果我们都能在 Google Doc 之类的软件中工作,那就会变得非常简单”

已接受的挑战!

过去几天,我很高兴构建了一个投资回报模拟器,我们可以根据多个参数预测投资回报。

互联网上充满了这样的网站,但在这个网站上,我想探索如何**嵌入协作功能**,以及它们如何**将人们聚集在一起**。

在本指南中,我们将了解如何在短短 10 分钟内将实时协作添加到常规 React 应用程序中,而无需编写任何后端代码或处理 Web 套接字。作为奖励,我们还将探索如何增强协作网站的用户体验!

该项目的完整源代码可在 GitHub 上找到

让我们开始吧!

起点:打好基础

在深入研究协作功能之前,我们需要一个坚实的基础。我首先创建了一个单人版本,用户可以在其中输入他们的投资参数。然后,这些输入将输入到计算引擎中,该引擎会生成并显示投资回报预测。

我使用 bolt.new 快速启动并运行。它给我的简洁设计以及我快速到达可接受起点给我留下了深刻印象。尽管领先优势很大,但我仍然需要微调相当多的计算逻辑,并根据我的喜好调整 UI。

使应用程序协作

单人版完成后,我把注意力转向了协作。这个项目为测试 React Together 提供了绝佳的机会,React Together 是我过去几个月在 Multisynq 开发的开源库。

React Together 提供了钩子和组件,可实现协作功能,而无需设置后端或管理套接字连接的复杂性。实施过程很简单,但它揭示了一些需要改进的地方,我们将在库的未来版本中解决这些问题。

现在,让我们逐步了解向我们的应用添加实时协作的三个步骤!启动您的计时器 😉

步骤 1:设置 React Together 上下文

第一步是将我们的应用程序包装在 React Together 上下文提供程序中。此组件在后台处理所有状态同步和会话管理。

// src/index.tsx
import { StrictMode } from 'react'
import { ReactTogether } from 'react-together'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  
    
      
    
  ,
)

React Together 使用 Multisynq 的基础架构进行应用程序同步,这需要 API 密钥。您可以从 multisynq.io/account 获取 API 密钥。不用担心,这些密钥是公开的,因为您可以控制哪些域可以使用它们。

我们可以配置 React Together,让所有用户在进入网站后自动连接到同一个会话。事实上,这将使本指南分为两步,但我选择了 Google Docs 风格的方法,其中协作是可选的。用户保持断开连接,直到他们通过单击按钮明确创建或加入会话。我们将在本指南的第三步介绍会话管理!

步骤 2:在用户之间同步状态

设置好 React Together 后,下一步就是在用户之间同步状态。这个过程非常简单:我们只需要将 React 的 `useState` 钩子替换为 React Together 的 `useStateTogether` 钩子。

`useStateTogether` 钩子的工作原理与 `useState` 类似,但需要额外的 `rtKey` 参数。此键唯一地标识了整个应用程序的状态,即使在响应式布局中(其中 DOM 层次结构可能在视口之间有所不同),也能确保正确同步。

转换过程如下:

// Before
import { useState } from 'react'

export default function Calculator() {
  const [startingAmount, setStartingAmount] = useState(20000);
  const [years, setYears] = useState(25);
  const [returnRate, setReturnRate] = useState(10);
  const [compoundFrequency, setCompoundFrequency] = useState("annually");
  const [additionalContribution, setAdditionalContribution] = useState(500);
  const [contributionTiming, setContributionTiming] = useState("beginning");
  const [contributionFrequency, setContributionFrequency] = useState("month");

  // ...
}
// After
import { useStateTogether } from 'react-together'

export default function Calculator() {
  const [startingAmount, setStartingAmount] = useStateTogether("startingAmount", 20000);
  const [years, setYears] = useStateTogether("years", 25);
  const [returnRate, setReturnRate] = useStateTogether("returnRate", 10);
  const [compoundFrequency, setCompoundFrequency] = useStateTogether("compoundFrequency", "annually");
  const [additionalContribution, setAdditionalContribution] = useStateTogether("additionalContribution", 500);
  const [contributionTiming, setContributionTiming] = useStateTogether("contributionTiming", "beginning");
  const [contributionFrequency, setContributionFrequency] = useStateTogether("contributionFrequency", "month");

  // ...
}

这种方法的优点在于应用程序可以继续像以前一样工作 - 唯一的区别是现在状态更新在所有连接的用户之间同步。

步骤 3:添加会话管理

最后一步是为用户添加创建、加入和离开协作会话的方法。我选择通过计算器上方的标题部分来实现这一点,让每个人都能轻松看到会话控件。

Visible session management

React Together 通过提供四个基本钩子使这一点变得简单:

  • useIsTogether:告诉我们当前是否处于会话中
  • useCreateRandomSession:创建一个新的私人会话
  • useLeaveSession:断开当前会话
  • useJoinUrl:提供可共享的 URL 供其他人加入
  • 这是标题组件的简化版本(我只是删除了类名):

    import { Button, Typography } from "antd";
    import { Calculator as CalculatorIcon } from "lucide-react";
    import { useCreateRandomSession, useIsTogether, useJoinUrl, useLeaveSession } from "react-together";
    
    export function Header() {
      const isTogether = useIsTogether();
      const leaveSession = useLeaveSession();
      const createRandomSession = useCreateRandomSession();
      const joinUrl = useJoinUrl();
    
      return (
        {isTogether ? (
          

    Invite your friends to this session with the link below:

    {joinUrl}
    ) : (

    Want to collaborate with your friends?

    )} ); }

    通过此实现,用户现在只需单击一下即可开始协作会话。当有人使用共享 URL 加入时,他们将立即看到与其他人相同的状态,所有更改都会实时同步到所有参与者。

    就是这样,很简单,而且有效!而且,不到 10 分钟就可以完成!

    增强协作体验

    虽然基本同步功能运行良好,但还是感觉有些不对劲:页面上的元素“自行”发生变化,没有任何迹象表明是谁在进行更改。这是协作应用程序面临的一个常见挑战,而 Google Docs 等工具通过显示其他用户正在查看和编辑的位置解决了这一问题。

    真正的协作不仅仅是同步状态,而是要创造一种临场感。用户需要“看到”彼此才能有效地合作。

    我最初考虑实现共享光标,让用户可以看到彼此的鼠标指针。然而,这种方法在响应式 Web 应用程序中存在挑战:

  • 鼠标坐标在不同的视口大小之间不能清晰地映射
  • 光标位置通常缺乏上下文——不清楚为什么用户的光标位于特定位置
  • 在不同的屏幕布局中,光标位置的含义可能会不明确
  • 相反,我专注于我们真正想通过用户存在实现的目标:

  • 帮助用户感受到其他人的积极存在
  • 显示每个用户当前正在查看或编辑的元素
  • 解决方案是什么?突出显示用户正在交互的元素。这种方法更简单、更直观,并且适用于所有视口尺寸。让我们看看如何在两个关键领域实现这一点:图表选项卡和输入字段。

    将用户状态添加到图表选项卡

    让我们从用户存在的简单实现开始:显示哪些用户正在查看每个图表选项卡。

    为此,我们需要一种特殊的共享状态,其中每个用户都可以拥有自己的、对其他人可见的价值。

    React Together 的 `useStateTogetherWithPerUserValues` 钩子正好满足了我们的需求(是的,这个钩子的名字很拗口!)。此钩子的工作原理与 `useStateTogether` 类似,但它不共享单个值,而是允许每个用户拥有自己的值,并且所有参与者都可以看到这些值。此钩子返回三个元素:

  • 当前用户的本地状态值
  • 更新本地状态的函数
  • 包含每个用户状态值的对象
  • 以下是我们如何实现在标签旁边显示用户头像:

    export default function Charts({ results }: ChartsProps) {
      const myId = useMyId();
    
      // Track which tab each user is viewing
      const [activeKey, setActiveKey, activeKeyPerUser] =
        useStateTogetherWithPerUserValues("active-keys", "1");
    
      const tabs = [
        {
          key: "1",
          label: "Growth Forecast",
          children: ,
        },
        {
          key: "2",
          label: "Investment Breakdown",
          children: ,
        },
      ];
    
      return (
         setActiveKey(key)}
          items={tabs.map(({ key, label, ...rest }) => ({
            key,
            label: (
               id !== myId && activeKeyPerUser[id] === key
                )}
              />
            ),
            ...rest,
          }))}
        />
      );
    }
    
    // This component renders the tab label with user avatars
    function PresenceLabel({ label, userIds }: PresenceLabelProps) {
      return (
        
    {label} {userIds.map((userId) => ( ))}
    ); }

    在上面的代码片段中,我们用 `useStateTogetherWithPerUserValues` 替换了 `useState`,再次,应用程序继续像以前一样工作,但现在每个人都可以看到其他人的状态!然后我们只需要呈现我们刚刚获得的新信息。

    此实现在每个选项卡旁边显示用户头像,从而清楚地显示哪些用户正在查看哪些图表。我们过滤掉当前用户的头像以避免冗余,因为用户不需要看到自己的状态指示器。

    在输入字段中添加用户存在

    向输入字段添加状态指示器遵循与上一个示例类似的模式,但有一个额外的要求:我们需要跟踪用户何时开始和停止编辑。幸运的是,Ant Design 的组件为此提供了必要的回调。

    对于每个输入字段,我想要:

  • 当其他人正在编辑时显示彩色边框
  • 在右上角显示编辑者的头像
  • 在整个应用程序中保持每个用户一致的颜色
  • 以下是我们使用 `useStateTogetherWithPerUserValues` 钩子实现这一点的方法:

    import { Avatar, Badge, Typography } from "antd";
    import { useMyId, useStateTogetherWithPerUserValues } from "react-together";
    import { getUserColor, getUserAvatarUrl } from "../utils/random";
    
    const { Text } = Typography;
    
    export function PresenceEditableText({ rtKey, value, onChange }) {
      const myId = useMyId();
    
      const [isEditing, setIsEditing, isEditingPerUser] =
        useStateTogetherWithPerUserValues(rtKey, false);
    
      const othersEditing = Object.entries(isEditingPerUser)
        .filter(([userId, isEditing]) => userId !== myId && isEditing)
        .map(([userId]) => userId);
    
      // Highlight with the first editor's color if we're not editing
      const borderColor = !isEditing && othersEditing.length > 0
        ? getUserColor(othersEditing[0]) 
        : "transparent";
    
      return (
        
                {othersEditing.map((userId) => (
                  
                ))}
              
            ) : null
          }
        >
          
             setIsEditing(true),
                onChange: (v) => {
                  onChange(v);
                  setIsEditing(false);
                },
                onEnd: () => setIsEditing(false),
                onCancel: () => setIsEditing(false)
              }}
            >
              {value}
            
          
        
      );
    }

    虽然代码稍微长一点,但原理很简单:我们只需要跟踪哪些用户正在编辑每个输入字段,然后渲染我们想要的可视化效果。

    这种方法也适用于任何其他输入类型,例如下拉菜单和滑动条!

    --

    就这样!有了这个完全协作的投资回报模拟器,我的朋友可以更轻松地通过 Zoom 向他的儿子解释投资回报是如何运作的。任务完成了!✨

    看到创建这种协作网站如此简单,我不禁想知道当我们在线时互联网如何让我们更加紧密地联系在一起......稍后会详细介绍!

    希望您学到了新知识,如果您有任何反馈或疑问,请随时联系我们!

    祝你编码愉快!🚀

  • 👉 现场演示
  • 🧑‍💻 源代码
  • 📚 React Together 文档