This is an experimental learning tutorial demonstrating the integration of Fluid into create-react-app
.
Concepts you will learn:
- How to integrate Fluid into a React application
- How to run and connect your application to a local Fluid service (Tinylicious)
- How to create and get Fluid Containers and collaborative objects
- How to use a SharedMap distributed data structure (DDS) to sync data between connected clients
* Just want to see the code? Jump to the finished tutorial..
In this demo you will be doing the following:
- Install Create-React-App
- Install Fluid Package Dependencies
- Import and Initialize Dependencies
- Get Fluid Data
- Update the view
- Next Steps
npx create-react-app my-app-name --use-npm
cd my-app-name
npx create-react-app my-app-name
cd my-app-name
The tinylicious
server will be required for Fluid to work.
npx tinylicious
Open up a new terminal tab and start up our React app
npm run start
There are two packages to install to get started with Fluid:
@fluid-experimental/tinylicious-client
- Defines the service connection for our local Fluid server and starting schema for the container
@fluidframework/map
- Contains the SharedMap you will use to sync data
npm install @fluid-experimental/tinylicious-client @fluidframework/map
yarn add @fluid-experimental/tinylicious-client @fluidframework/map
* These are still experimental packages, and not ready for production
Lastly, open up the App.js
file, as that will be the only file we need to edit.
TinyliciousClient
is the service we will use to connect to our local Tinylicious server. It also provides methods to create a Fluid container with a set of initial DataObjects or DDSes that are defined in the containerSchema
.
The Fluid container interacts with the processes and distributes operations, manages the lifecycle of Fluid objects, and provides a request API for accessing Fluid objects.
SharedMap
is the DDS that we will initialize on our container.
// App.js
// Add to the top of the file
import React from "react";
import TinyliciousClient from "@fluid-experimental/tinylicious-client";
import { SharedMap } from "@fluidframework/map";
Fluid collaboration happens in containers, which have unique identifiers (like a document filename). For this example we'll use the hash part of the URL as the identifier, and generate a new hash if there isn't one present already. The getContainerId
function will automate this for us.
// add below imports
const getContainerId = () => {
let isNew = false;
if (window.location.hash.length === 0) {
isNew = true;
window.location.hash = Date.now().toString();
}
const containerId = window.location.hash.substring(1);
return { containerId, isNew };
};
TinyliciousClient
needs to be initialized before use and can take an optional configuration object to changes settings such as default port. Depending on your deploy target, you might initialize and use different client packages.
// add below getContainerId
TinyliciousClient.init();
Before we can access any Fluid data, we need to make a call to the TinyliciousClient
with necessary service configuration and container schema.
serviceConfig
is going to vary per service. With Tinylicious it will only requires anid
.containerSchema
is going to include a stringname
and a collection of the data types our application will use.
The following getFluidData
function utilizes the getContainerId
to return a unique ID and determine if this is an existing document (getContainer
) or if we need to create a new one (createContainer
).
Since this function is an async, we'll need to wait for the initialObjects
to be returned. Once returned, each initialObjects
key will point to a connected data structure as per defined in the schema.
// after the init()
const getFluidData = async () => {
const { containerId, isNew } = getContainerId();
const serviceConfig = {id: containerId};
const containerSchema = {
name: 'cra-demo-container',
initialObjects: { mySharedMap: SharedMap }
};
const fluidContainer = isNew
? await TinyliciousClient.createContainer(serviceConfig, containerSchema)
: await TinyliciousClient.getContainer(serviceConfig, containerSchema);
// returned initialObjects are live Fluid data structures
return fluidContainer.initialObjects;
}
Now that we've defined how to get our Fluid data, we need to tell React to call getFluidData
on load and then store the result in state.
React's useState
will provide the storage we need, and useEffect
will allow us to call getFluidData
on render, passing the returned value into fluidData
. By setting an empty dependency array at the end of the useEffect
, we ensure that this function only gets called once.
// Add to the top of our App
const [fluidData, setFluidData] = React.useState();
React.useEffect(() => {
// Get/Create container and return live Fluid data
getFluidData().then(data => setFluidData(data));
}, []);
Syncing our Fluid and View data requires that we set up an event listener, which is another usecase for useEffect
. This second useEffect
function will return early if fluidData
is not defined and be ran again once fluidData
has been set thanks to the added dependency.
To sync the data we're going to create a syncView
function, call that function once to initialize the data, and then keep listening for the mySharedMap
"valueChanged" event, and fire the function again each time. Now React will handle updating the view each time the new viewData
state is modified.
// Add below the previous useEffect
const [viewData, setViewData] = React.useState();
React.useEffect(() => {
if (!fluidData) return;
const { mySharedMap } = fluidData;
// sync Fluid data into view state
const syncView = () => setViewData({ time: mySharedMap.get("time") });
// ensure sync runs at least once
syncView();
// update state each time our map changes
mySharedMap.on("valueChanged", syncView);
return () => { mySharedMap.off("valueChanged", syncView) }
}, [fluidData])
In this simple multi-user app, we are going to build a button that, when pressed, shows the current timestamp. We will store that timestamp in Fluid. This allows co-authors to automatically see the most recent timestamp at which any author pressed the button.
To make sure we don't render the app too soon, we return a blank <div />
until the map
is defined. Once that's done, we'll render a button that sets the time
key in our map
to the current timestamp. Each time this button is pressed, every user will see the latest value stored in the time
state variable.
// update the App return
if (!viewData) return <div/>;
return (
<div className="App">
<button onClick={() => fluidData.mySharedMap.set("time", Date.now().toString())}>
click
</button>
<span>{viewData.time}</span>
</div>
)
When the app loads it will update the URL. Copy that new URL into a second browser and note that if you click the button in one browser, the other browser updates as well.
- Try extending the demo with more key/value pairs and a more complex UI
npm install @fluentui/react
is a great way to add UI controls
- Try using other DDSes such as the SharedString