Using Fluid with React
In this tutorial, you’ll learn about using the Fluid Framework by building a simple application that enables every client of the application to change a dynamic time stamp on itself and all other clients almost instantly. You’ll also learn how to connect the Fluid data layer with a view layer made in React .
To jump ahead into the finished demo, check out the React demo in our FluidExamples repo .
The following image shows the time stamp application open in four browsers. Each has a button labeled click and beside it a Unix epoch time. The same time is in all four. The cursor is on the button in one browser.
The following image shows the same four clients one second after the click button was pressed. Note that the timestamp has updated to the very same time in all four browsers.
Note
This tutorial assumes that you are familiar with the Fluid Framework Overview and that you have completed the Quick Start . You should also be familiar with the basics of React , creating React projects , and React Hooks .
Create the project
-
Open a Command Prompt and navigate to the parent folder where you want to create the project; e.g.,
c:\My Fluid Projects
. -
Run the following command at the prompt. (Note that the CLI is npx, not npm. It was installed when you installed Node.js.)
npx create-react-app fluid-react-tutorial --use-npm
-
The project is created in a subfolder named
fluid-react-tutorial
. Navigate to it with the commandcd fluid-react-tutorial
. -
The project uses two Fluid libraries:
Library Description fluid-framework
Contains the SharedMap distributed data structure that synchronizes data across clients. This object will hold the most recent timestamp update made by any client. @fluidframework/tinylicious-client
Defines the connection to a Fluid service server and defines the starting schema for the Fluid container . Run the following command to install the libraries.
npm install @fluidframework/tinylicious-client fluid-framework
Code the project
-
Open the file
\src\App.js
in your code editor. Delete all the defaultimport
statements except the one that importsApp.css
. Then delete all the markup from thereturn
statement. The file should look like the following:import './App.css'; function App() { return ( ); } export default App;
-
Add the following
import
statements:import React from "react"; import { TinyliciousClient } from "@fluidframework/tinylicious-client"; import { SharedMap } from "fluid-framework";
Move Fluid data to the view
-
The Fluid runtime will bring changes made to the timestamp from any client to the current client. But Fluid is agnostic about the UI framework. You can use a helper method to get the Fluid data, from the SharedMap object, into the view layer (the React state). Add the following code below the import statements. This method is called when the application loads the first time, and the value that is returned form it is assigned to a React state property.
const getFluidData = async () => { // TODO 1: Configure the container. // TODO 2: Get the container from the Fluid service. // TODO 3: Return the Fluid timestamp object. }
-
Replace
TODO 1
with the following code. Note that there is only one object in the container: a SharedMap holding the timestamp. Note also thatsharedTimestamp
is the ID of theSharedMap
object and it must be unique within the container.const client = new TinyliciousClient(); const containerSchema = { initialObjects: { sharedTimestamp: SharedMap } };
-
Replace
TODO 2
with the following code. Note thatcontainerId
is being stored on the URL hash, and if there is nocontainerId
we create a new container instead.let container; const containerId = location.hash.substring(1); if (!containerId) { ({ container } = await client.createContainer(containerSchema)); const id = await container.attach(); location.hash = id; } else { ({ container } = await client.getContainer(containerId, containerSchema)); }
-
Replace
TODO 3
with the following code.return container.initialObjects;
Get the Fluid data on application startup
Now that we’ve defined how to get our Fluid data, we need to tell React to call getFluidData
when the application starts up and then store the result in state. So add the following code at the top of the App()
function (above the return
statement). Note about this this code:
- By setting an empty dependency array at the end of the useEffect, we ensure that this function only gets called once.
- Since
setFluidSharedObjects
is a state-changing method, it will cause the ReactApp
component to immediately rerender.
const [fluidSharedObjects, setFluidSharedObjects] = React.useState();
React.useEffect(() => {
getFluidData()
.then(data => setFluidSharedObjects(data));
}, []);
Keep the view synchronized with the Fluid data
The timestamp that is rendered in the application’s UI does not come directly from the fluidSharedObjects
state object because that object can be changed by other clients and these changes do not call the setFluidSharedObjects
method, so they do not trigger a rerender of the App
component. Thus, remote changes would not appear in the current client’s UI.
To ensure that both local and remote changes to the timestamp are reflected in the UI, create a second application state value for the local timestamp and ensure that it is updated (with a state-updating function) whenever any client changes the fluidSharedObjects
value.
-
Below the preceding
useEffect
add the following code. Note about this code:- The
fluidSharedObjects
state is undefined only when theApp
component is rendering for the first time. - Passing
fluidSharedObjects
in the second parameter of theuseEffect
hook ensures that the hook will not pointlessly run iffluidSharedObjects
has not changed since the last time theApp
component rendered.
const [localTimestamp, setLocalTimestamp] = React.useState(); React.useEffect(() => { if (fluidSharedObjects) { // TODO 4: Set the value of the localTimestamp state object that will appear in the UI. // TODO 5: Register handlers. // TODO 6: Delete handler registration when the React App component is dismounted. } else { return; // Do nothing because there is no Fluid SharedMap object yet. } }, [fluidSharedObjects])
- The
-
Replace
TODO 4
with the following code. Note about this code:- The Fluid
SharedObject.get
method returns the data of theSharedObject
(in this case theSharedMap
object), which is roughly theSharedObject
without any of its methods. So thesetLocalTimestamp
function is setting thelocalTimestamp
state to a copy of the data of theSharedMap
object. (The key “time” that is passed toSharedObject.get
is created in a later step. It will have been set by the time this code runs the first time.) updateLocalTimestamp
is called immediately to ensure thatlocalTimestamp
is initialized with the current shared timestamp value.
const { sharedTimestamp } = fluidSharedObjects; const updateLocalTimestamp = () => setLocalTimestamp({ time: sharedTimestamp.get("time") }); updateLocalTimestamp();
- The Fluid
-
To ensure that the
localTimestamp
state is updated whenever thesharedTimestamp
is changed even by other clients, replaceTODO 5
with the following code. Note that becauseupdateLocalTimestamp
calls the state-setting functionsetTimestamp
, a rerender is triggered whenever any client changes the FluidsharedTimestamp
.sharedTimestamp.on("valueChanged", updateLocalTimestamp);
-
It is a good practice to deregister event handlers when the React component dismounts, so replace
TODO 6
with the following code.return () => { sharedTimestamp.off("valueChanged", updateLocalTimestamp) }
Create the view
Below the useEffect
hook, replace the return ();
line with the following code. Note about this code:
- If the
localTimestamp
state has not been initialized, a blank screen is rendered. - The
sharedTimestamp.set
method sets the key of thesharedTimestamp
object to “time” and the value to the current UNIX epoch time. This triggers thevalueChanged
event on the object, so theupdateLocalTimestamp
function runs and sets thelocalTimestamp
state to the same object; for example,{time: "1615996266675"}
. TheApp
component rerenders and the<span>
is updated with the latest timestamp. - All other clients update too because the Fluid server propagates the change to the
sharedTimestamp
on all of them and thisvalueChanged
event updates thelocalTimestamp
state on all of them.
if (localTimestamp) {
return (
<div className="App">
<button onClick={() => fluidSharedObjects.sharedTimestamp.set("time", Date.now().toString())}>
Get Time
</button>
<span>{localTimestamp.time}</span>
</div>
)
} else {
return <div/>;
}
Start the Fluid server and run the application
In the Command Prompt, run the following command to start the Fluid service. Note that tinylicious
is the name of the Fluid service that runs on localhost.
npx tinylicious
Open a new Command Prompt and navigate to the root of the project; for example, C:/My Fluid Projects/fluid-react-tutorial
. Start the application server with the following command. The application opens in your browser. This may take a few minutes.
npm run start
Paste the URL of the application into the address bar of another tab or even another browser to have more than one client open at a time. Press the Get Time button on any client and see the value change and synchronize on all the clients.
Next steps
- Try extending the demo with more key/value pairs and a more complex UI.
- Consider using the Fluent UI React controls
to give the application the look and feel of Microsoft 365. To install them in your project run the following in the command prompt:
npm install @fluentui/react
. - Try changing the container schema to use a different shared data object type or specify multiple objects in
initialObjects
. - For an example that will scale to larger applications and larger teams, check out the React Starter Template in the FluidExamples repo .
Tip
When you make changes to the code the project will automatically rebuild and the application server will reload. However, if you make changes to the container schema, they will only take effect if you close and restart the application server. To do this, give focus to the Command Prompt and press Ctrl-C twice. Then run npm run start
again.