1.4.0-preview.0 • Published 1 month ago

@microsoft/live-share-react v1.4.0-preview.0

Weekly downloads
-
License
SEE LICENSE IN LI...
Repository
github
Last release
1 month ago

Live Share React

DISCLAIMER: This package is in preview and experimental. We are not yet committed to maintaining this package and may make breaking changes at any time. Read this package's license for more information.

Live Share React is designed to make building collaborative apps in React simple and intuitive, all using familiar patterns from functional React.

Here is a simple example of how to get started:

// LiveShareApp.jsx
import { LiveShareProvider } from "@microsoft/live-share-react";
import { LiveShareHost } from "@microsoft/teams-js";

const host = LiveShareHost.create();

export function LiveShareApp() {
  // Call app.initialize() from teams-js before rendering LiveShareProvider

  return (
    <LiveShareProvider
      joinOnLoad={true}
      host={host}
    >
      <SharedCheckbox />
    </LiveShareProvider>
  );
}

// SharedCheckbox.jsx
import { useSharedState } from "@microsoft/live-share-react";

export function SharedCheckbox() {
  const [checked, setChecked] = useSharedState("MY-UNIQUE-ID", false);
  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={() => {
        setChecked(!checked);
      }}
    />
  );
}

Getting started

Installing

To add the latest version of the SDK to your application using NPM:

npm install fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react --save

or using Yarn:

yarn add fluid-framework @fluidframework/azure-client @microsoft/live-share @microsoft/live-share-media @microsoft/live-share-canvas @microsoft/live-share-react

Building package

After cloning the GitHub repository, navigate to the root folder and perform:

npm install
npm run build

This will use npm workspaces to hoist and build all dependencies.

Sample app

After installing/building the packages, you can also try out a working sample here.

Live Share React vs. Vanilla Fluid

Fluid Framework and Live Share are powerful frameworks that can greatly simplify the effort of building collaborative applications, but for many React applications, it can take time to get used to. This experimental package aims to strip away as much of that learning curve as possible.

Where traditional Fluid utilizes a developer-defined object schema with a hierarchal structure, this package abstracts that out on your behalf. While you can still tap into more complicated hierarchies with this library, it is designed to behave more like blob storage / NoSQL.

To use Fluid's distributed-data structures (DDS) or Live Share's live objects, you can simply use the corresponding React hook, providing a unique identifier for that DDS. If no DDS exists when the component is first mounted, we automatically create one for you. Otherwise, we will connect to the existing one.

Much like React itself, this package is opinionated, and it may not be for everyone. To learn more about using Live Share the traditional way, see our Live Share README.

Types of hooks

Live Share supports all of the live data structures provided through Live Share, and most of the officially supported DDS's available through Fluid Framework. If you have custom data objects, this package also exposes some underlying APIs for building your own custom React hooks.

Here are the hooks provided by this library:

useSharedState

Inspired by React's own useState hook, useSharedState should feel familiar to React developers. Under the hood, this hook uses a Fluid SharedMap dedicated for useSharedState, listening for changes to the key provided and automatically updating the shared state with any changes. And yes, if two components in the same application use the same key, those components will be in sync with each other!

Since these keys are a string you provide while calling useSharedState, you can dynamically load these into your app as needed. With this in mind, we've also provided an optional disposeState action if the state is no longer relevant to your app. If you don't dispose it, then the data will persist in the container should you access it later (up to the lifetime of the Fluid container).

The following example shows how useSharedState can be used to dynamically create collaborative features in your app on the fly:

import { useSharedState } from "@microsoft/live-share-react";

export function CounterCard({ card, onDelete }) {
  const [count, setCount, disposeCount] = useSharedState(
    `card-count:${card.id}`,
    0
  );
  return (
    <div className="card">
      <h3>{card.title}</h3>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        {"+1"}
      </button>
      <span>{`${count} `}</span>
      <button
        onClick={() => {
          onDelete(card.id);
          disposeCount();
        }}
      >
        {"Delete"}
      </button>
    </div>
  );
}

NOTE: While this hook will get you pretty far on its own, carefully consider which of our React hook is best for your scenario.

useSharedMap

This hook loads a Fluid SharedMap corresponding with the key you provide. Compared to useSharedState, this hook allows you to render a collection of items with less risk of conflict when multiple users may be writing to the same object simultaneously. For example, rather than storing an entire list of objects in a single key of useSharedState, the map allows each item in the list to have its own unique key.

While you write to keys individually, the map object exposed through the hook uses React state itself. Lets see an example in action:

import { useSharedMap } from "@microsoft/live-share-react";
import { v4 as uuid } from "uuid";

export function CardList() {
  const { map, setEntry, deleteEntry } = useSharedMap("CUSTOM-MAP-ID");
  return (
    <div>
      <h2>{"Cards"}</h2>
      <button
        onClick={() => {
          const id = uuid();
          setEntry(id, {
            id,
            title: "Custom Card",
          });
        }}
      >
        {"+ Add card"}
      </button>
      <div className="flex wrap row hAlign">
        {[...map.values()].map((cardValue) => (
          <CounterCard
            key={cardValue.id}
            card={cardValue}
            onDelete={deleteEntry}
          />
        ))}
      </div>
    </div>
  );
}

useLivePresence

Presence makes it easy to track which users are currently in the session and assign custom data to them.

import { useLivePresence } from "@microsoft/live-share-react";
import { PresenceState } from "@microsoft/live-share";

export function OnlineUsers() {
  const { localUser, allUsers, updatePresence } = useLivePresence(
    "UNIQUE-PRESENCE-KEY", // required unique key for presence
    { favoriteColor: "red" } // optional custom data object
  );
  return (
    <div>
      <h1>{"Online Users:"}</h1>
      <div>
        {allUsers
          .filter((user) => user.state === PresenceState.online)
          .map((user) => (
            <div key={user.userId}>
                {user.displayName + " " + user.data!.favoriteColor}
            </div>
          ))}
      </div>
      <button
        onClick={() => {
          updatePresence(
            localUser.data,
            localUser?.state === PresenceState.offline
              ? PresenceState.online
              : PresenceState.offline,
          );
        }}
      >
        {`Toggle status`}
      </button>
    </div>
  );
}

useLiveState

Unlike useSharedState, useLiveState is only stateful while one or more users are connected to it. This can make it easy to have state that behaves more closely to a regular React useState, when desireable.

This component also features role verification, which allows you to choose the Teams meeting roles which are eligible to edit the state, if needed.

import { useLiveState } from "@microsoft/live-share-react";
import { UserMeetingRole } from "@microsoft/live-share";

const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter ];

export function AppState() {
  const [state, setState] = useLiveState("CUSTOM-STATE-ID", ExampleAppState.WAITING, ALLOWED_ROLES);

  if (state === ExampleAppState.WAITING) {
    return (
      <div>
        <h2>{"Waiting"}</h2>
        <button
          onClick={() => {
            setState(ExampleAppState.START, {
              startedBy: "First Last",
            });
          }}
        >
          {"Start"}
        </button>
      </div>
    );
  }
  return (
    <div>
      <h2>{`Started by: ${data!.startedBy}`}</h2>
      <button
        onClick={() => {
          setState(ExampleAppState.WAITING, undefined);
        }}
      >
        {"End"}
      </button>
    </div>
  );
};

useLiveEvent

If you want to generic JSON between clients that is completely transient, then useLiveEvent is a great choice. A good example of that is a reactions feature similar to that in Microsoft Teams meetings, since people joining a meeting late don't need to see reactions from earlier in the meeting.

Here is a simple example:

import { useLiveEvent } from "@microsoft/live-share-react";

export function Reactions() {
  const { latestEvent, sendEvent } = useLiveEvent("EVENT-ID");

  return (
    <div>
      {/* Buttons for sending reactions */}
      <button
        onClick={() => {
          sendEvent({ emoji: "❤️" });
        }}
      >
        {"❤️"}
      </button>
      <button
        onClick={() => {
          sendEvent({ emoji: "😂" });
        }}
      >
        {"😂"}
      </button>
      {/* Show latest reaction */}
      {latestEvent?.local === false && (
        <div>{`Received: ${latestEvent?.value.emoji}`}</div>
      )}
      {latestEvent?.local === true && (
        <div>{`Sent: ${latestEvent?.value.emoji}`}</div>
      )}
    </div>
  );
}

useLiveTimer

You can use the useLiveTimer hook to build a synchronized countdown timer. A good example of that might be a meditation timer or a countdown for a round in a group activity.

Here is a simple example:

import { useLiveTimer } from "@microsoft/live-share-react";

export function CountdownTimer() {
  const { milliRemaining, timerConfig, start, pause, play } = useLiveTimer("TIMER-ID");

  return (
    <div>
      <button
        onClick={() => {
          start(60 * 1000);
        }}
      >
        { timerConfig === undefined ? "Start" : "Reset" }
      </button>
      { timerConfig !== undefined && (
        <button
          onClick={() => {
            if (timerConfig.running) {
              pause();
            } else {
              play();
            }
          }}
        >
          {timerConfig.running ? "Pause" : "Play" }
        </button>
      )}
      { milliRemaining !== undefined && (
        <p>
          { `${Math.round(milliRemaining / 1000)} / ${Math.round(timerConfig.duration) / 1000}` }
        </p>
      )}
    </div>
  );
}

useMediaSynchronizer

If you want to synchronize video content, @microsoft/live-share-media is also supported by this package through the useMediaSynchronizer hook. Using any HTMLMediaPlayer element, or a delegate object matching our IMediaPlayer interface, you can easily build watch together capabilities into your app.

Let's see this in action:

import { useMediaSynchronizer } from "@microsoft/live-share-react";
import { UserMeetingRole } from "@microsoft/live-share";
import { useRef } from "react";

const ALLOWED_ROLES = [UserMeetingRole.organizer, UserMeetingRole.presenter];

const INITIAL_TRACK =
  "https://storage.googleapis.com/media-session/big-buck-bunny/trailer.mov";

export function VideoPlayer() {
  const videoRef = useRef(null);
  const { play, pause } = useMediaSynchronizer(
    "MEDIA-SESSION-ID",
    videoRef,
    INITIAL_TRACK,
    ALLOWED_ROLES
  );

  return (
    <div>
      <video ref={videoRef} />
      <button onClick={play}>{"Play"}</button>
      <button onClick={pause}>{"Pause"}</button>
    </div>
  );
}

useLiveCanvas

If you want to add turn-key inking & cursors, use the useLiveCanvas hook, powered by @microsoft/live-share-canvas.

Let's see this in action:

import { useLiveCanvas } from "@microsoft/live-share-react";
import { InkingTool } from "@microsoft/live-share-canvas";
import { useRef } from "react";

export const ExampleLiveCanvas = () => {
    const liveCanvasRef = useRef(null);
    const { liveCanvas, inkingManager } = useLiveCanvas(
        "CUSTOM-LIVE-CANVAS",
        liveCanvasRef,
    );

    return (
        {/** Canvas currently needs to be a child of a parent with absolute styling */}
        <div style={{ position: "absolute"}}>
            <div
                ref={liveCanvasRef}
                // Best practice is to not define inline styles
                style={{ width: "556px", height: "224px" }}
            />
            {!!liveCanvas && (
                <div>
                    <button
                        onClick={() => {
                            inkingManager.tool = InkingTool.pen;
                        }}
                    >
                        {"Pen"}
                    </button>
                    <button
                        onClick={() => {
                            inkingManager.tool = InkingTool.laserPointer;
                        }}
                    >
                        {"Laser pointer"}
                    </button>
                </div>
            )}
        </div>
    );
};

useTaskManager

If you want to ensure that only one user is responsible for a given task, you can use useTaskManager, which uses Fluid's TaskManager DDS.

Let's see this in action:

import { useTaskManager } from "@microsoft/live-share-react";

export const ExampleTaskManager = () => {
    const [taskId, setTaskId] = useState(undefined);
    const { lockedTask } = useTaskManager(
        "CUSTOM-TASK-MANAGER",
        taskId,
    );

    const displayText = lockedTask
        ? "You are assigned the task"
        : "Waiting for task assignment";

    return (
        <div>
            {!taskId && (
                <button
                    onClick={() => {
                        setTaskId("task-id")
                    }}
                >
                    {'Join task queue'}
                </button>
            )}
            {taskId && (
                <button
                    onClick={() => {
                        setTaskId(undefined)
                    }}
                >
                    {'Leave task queue'}
                </button>
            )}
            <p>{displayText}</p>
        </div>
    );
};

Custom Fluid object hooks

If you want to dynamically load a custom Fluid object in your app, use the useDynamicDDS to create a custom hook. This is the same hook that Live Share React uses internally within our custom hooks, such as useLiveEvent. If you made a custom data object or are using one of Fluid's experimental data structures, you also must register your Fluid LoadableObjectClass with DynamicObjectRegistry.registerObjectClass to @microsoft/live-share, if it is not already.

Implementations may vary for each dynamic object & hook. We will try and update this package periodically with new packages released by Fluid Framework and Live Share, as they are published.

Example:

import React from "react";
import { useDynamicDDS } from "@microsoft/live-share-react";
import { DynamicObjectRegistry } from "@microsoft/live-share";
import { TaskManager  } from "@fluid-experimental/task-manager";

// Register TaskManager as dynamic object
DynamicObjectRegistry.registerObjectClass(TaskManager, TaskManager.getFactory().type);

/**
 * A hook for joining a queue to lock tasks for a given id. Guaranteed to have only one user assigned to a task at a time.
 * 
 * @param uniqueKey the unique key for the TaskManager DDS
 * @param taskId the task id to lock
 * @returns stateful data about the status of the task lock
 */
export const useTaskManager = (uniqueKey: string, taskId?: string): {
    lockedTask: boolean;
    taskManager: TaskManager | undefined;
} => {
    /**
     * TaskId currently in queue for
     */
    const currentTaskIdRef = React.useRef<string | undefined>(undefined);
    /**
     * Stateful boolean that is true when the user is currently assigned the task
     */
    const [lockedTask, setLockedTask] = React.useState<boolean>(false);

    /**
     * User facing: dynamically load the TaskManager DDS for the given unique key.
     */
    const { dds: taskManager } = useDynamicDDS<TaskManager>(uniqueKey, TaskManager);

    /**
     * When the task id changes, lock the task. When the task id is undefined, abandon the task.
     */
    React.useEffect(() => {
        let mounted = true;
        if (taskManager) {
            if (taskId && currentTaskIdRef.current !== taskId) {
                if (currentTaskIdRef.current) {
                    taskManager.abandon(currentTaskIdRef.current);
                    setLockedTask(false);
                }
                currentTaskIdRef.current = taskId;
                const onLockTask = async () => {
                    try {
                        await taskManager.lockTask(taskId);
                        if (mounted) {
                            setLockedTask(true);
                        }
                    } catch {
                        if (mounted) {
                            setLockedTask(false);
                            currentTaskIdRef.current = undefined;
                        }
                    }
                }
                onLockTask();
            } else if (!taskId && currentTaskIdRef.current) {
                taskManager.abandon(currentTaskIdRef.current);
                setLockedTask(false);
                currentTaskIdRef.current = undefined;
            }
        }
        /**
         * When the component unmounts, abandon the task if it is still locked
         */
        return () => {
            mounted = false;
            if (currentTaskIdRef.current) {
                taskManager?.abandon(currentTaskIdRef.current);
            }
            currentTaskIdRef.current = undefined;
        }
    }, [taskManager, taskId]);

    return {
        lockedTask,
        taskManager,
    };
};

Code samples

Sample nameDescriptionJavascript
Live Share ReactSimple example with each of our custom Live Share React hooks.View

React version compatibility

This package is compatible with React versions ^16.8.0 and greater, including React v18. In order to ensure compatibility with different versions React, this project does not currently use React Suspense for data fetching on load. We are closely monitoring React guidelines and may post updates as this evolves further. If you have feedback or thoughts on this topic, join the discussion.

Package Compatibility

The Live Share SDK contains dependencies for @microsoft/teams-js and fluid-framework packages among others. Both of these packages are sensitive to the package version your app any libraries use. You will likely run into issues if the package version your app uses doesn't match the version other libraries you depend on use.

It is critical that your app use the package dependencies listed in the table below. Lookup the version of the @microsoft/live-share you're using and set any other dependencies in your package.json file to match:

@microsoft/live-share@microsoft/teams-jsfluid-framework@microsoft/live-share-*@fluidframework/azure-client@microsoft/TeamsFx@microsoft/TeamsFx-react
^1.0.0^2.11.0^1.2.3^1.0.0^1.0.0^2.5.0^2.5.0

Contributing

There are several ways you can contribute to this project:

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Reporting Security Issues

Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at secure@microsoft.com. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the Security TechCenter.

Copyright (c) Microsoft Corporation. All rights reserved.

Licensed under a special Microsoft License.

2.0.0-internal.5

1 month ago

2.0.0-internal.4

2 months ago

2.0.0-internal.3

2 months ago

2.0.0-internal.2

3 months ago

2.0.0-internal.1

3 months ago

1.4.0-preview.0

3 months ago

1.3.1-preview.0

3 months ago

1.3.0-preview.0

4 months ago

1.0.2-preview.1

9 months ago

1.2.2-preview.0

7 months ago

1.2.0-preview.1

7 months ago

1.2.0-preview.0

7 months ago

1.1.0-preview.0

8 months ago

1.2.1-preview.0

7 months ago

1.0.0-preview.24

12 months ago

1.0.1-preview.1

12 months ago

1.0.0-preview.22

12 months ago

1.0.0-preview.23

12 months ago

1.0.0-preview.9

1 year ago

1.0.0-preview.8

1 year ago

1.0.0-preview.7

1 year ago

1.0.0-preview.6

1 year ago

1.0.0-preview.5

1 year ago