How to: Use audience features in the Fluid Framework

In this tutorial, you'll learn about using the Fluid Framework Audience with React to create a visual demonstration of users connecting to a container. The audience object holds information related to all users connected to the container. In this example, the Azure Client library will be used to create the container and audience.

The following image shows ID buttons and a container ID input field. Leaving the container ID field blank and clicking a user ID button will create a new container and join as the selected user. Alternatively, the end-user can input a container ID and choose a user ID to join an existing container as the selected user.

A screenshot of a browser with buttons for selecting a user.

The next image shows multiple users connected to a container represented by boxes. The box outlined in blue represents the user who is viewing the client while the boxes outlined in black represents the other connected users. As new users attach to the container with unique ID's, the number of boxes will increase.

A screenshot of a browser showing information for four different container users.

Note

This tutorial assumes that you are familiar with the Fluid Framework Overview and that you have completed the QuickStart. You should also be familiar with the basics of React, creating React projects, and React Hooks.

Create the project

  1. Open a Command Prompt and navigate to the parent folder where you want to create the project; e.g., C:\My Fluid Projects.

  2. 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-audience-tutorial
    
  3. The project is created in a subfolder named fluid-audience-tutorial. Navigate to it with the command cd fluid-audience-tutorial.

  4. The project uses the following Fluid libraries:

    Library Description
    fluid-framework Contains the SharedMap distributed data structure that synchronizes data across clients.
    @fluidframework/azure-client Defines the connection to a Fluid service server and defines the starting schema for the Fluid container.
    @fluidframework/test-client-utils Defines the InsecureTokenProvider needed to create the connection to a Fluid Service.

    Run the following command to install the libraries.

    npm install @fluidframework/azure-client @fluidframework/test-client-utils fluid-framework
    

Code the project

Set up state variables and component view

  1. Open the file \src\App.js in the code editor. Delete all the default import statements. Then delete all the markup from the return statement. Then add import statements for components and React hooks. Note that we'll be implementing the imported AudienceDisplay and UserIdSelection components in the later steps. The file should look like the following:

        import { useState, useCallback } from "react";
        import { AudienceDisplay } from "./AudienceDisplay";
        import { UserIdSelection } from "./UserIdSelection";
    
        export const App = () => {
        // TODO 1: Define state variables to handle view changes and user input
        return (
        // TODO 2: Return view components
        );
        }
    
  2. Replace TODO 1 with the following code. This code initializes local state variables that will be used within the application. The displayAudience value determines if we render the AudienceDisplay component or the UserIdSelection component (see TODO 2). The userId value is the user identifier to connect to the container with and the containerId value is the container to load. The handleSelectUser and handleContainerNotFound functions are passed in as callbacks to the two views and manage state transitions. handleSelectUser is called when attempting to create/load a container. handleContainerNotFound is called when creating/loading a container fails.

    Note, the values userId and containerId will come from a UserIdSelection component through the handleSelectUser function.

        const [displayAudience, setDisplayAudience] = useState(false);
        const [userId, setUserId] = useState();
        const [containerId, setContainerId] = useState();
    
        const handleSelectUser = useCallback((userId, containerId) => {
        setDisplayAudience(true)
        setUserId(userId);
        setContainerId(containerId);
        }, [displayAudience, userId, containerId]);
    
        const handleContainerNotFound = useCallback(() => {
        setDisplayAudience(false)
        }, [setDisplayAudience]);
    
  3. Replace TODO 2 with the following code. As stated above, the displayAudience variable will determine if we render the AudienceDisplay component or the UserIdSelection component. Also, functions to update the state variables are passed into components as properties.

        (displayAudience) ?
        <AudienceDisplay userId={userId} containerId={containerId} onContainerNotFound={handleContainerNotFound}/> :
        <UserIdSelection onSelectUser={handleSelectUser}/>
    

Set up AudienceDisplay component

  1. Create and open a file \src\AudienceDisplay.js in the code editor. Add the following import statements:

        import { useEffect, useState } from "react";
        import { SharedMap } from "fluid-framework";
        import { AzureClient } from "@fluidframework/azure-client";
        import { InsecureTokenProvider } from "@fluidframework/test-client-utils";
    

    Note that the objects imported from the Fluid Framework library are required for defining users and containers. In the following steps, AzureClient and InsecureTokenProvider will be used to configure the client service (see TODO 1) while the SharedMap will be used to configure a containerSchema needed to create a container (see TODO 2).

  2. Add the following functional components and helper functions:

        const tryGetAudienceObject = async (userId, userName, containerId) => {
        // TODO 1: Create container and return audience object
        }
    
        export const AudienceDisplay = (props) => {
        //TODO 2: Configure user ID, user name, and state variables
        //TODO 3: Set state variables and set event listener on component mount
        //TODO 4: Return list view
        }
    
        const AudienceList = (data) => {
        //TODO 5: Append view elements to list array for each member
        //TODO 6: Return list of member elements
        }
    

    Note that the AudienceDisplay and AudienceList are functional components which handle getting and rendering audience data while the tryGetAudienceObject method handles the creation of container and audience services.

Getting container and audience

You can use a helper function to get the Fluid data, from the Audience object, into the view layer (the React state). The tryGetAudienceObject method is called when the view component loads after a user ID is selected. The returned value is assigned to a React state property.

  1. Replace TODO 1 with the following code. Note that the values for userId userName containerId will be passed in from the App component. If there's no containerId, a new container is created. Also, note that the containerId is stored on the URL hash. A user entering a session from a new browser may copy the URL from an existing session browser or navigate to localhost:3000 and manually input the container ID. With this implementation, we want to wrap the getContainer call in a try catch in the case that the user inputs a container ID which doesn't exist. Visit the Containers documentation for more information.

        const userConfig = {
            id: userId,
            name: userName,
            additionalDetails: {
                email: userName.replace(/\s/g, "") + "@example.com",
                date: new Date().toLocaleDateString("en-US"),
            },
        };
    
        const serviceConfig = {
            connection: {
                type: "local",
                tokenProvider: new InsecureTokenProvider("", userConfig),
                endpoint: "http://localhost:7070",
            },
        };
    
        const client = new AzureClient(serviceConfig);
    
        const containerSchema = {
            initialObjects: { myMap: SharedMap },
        };
    
        let container;
        let services;
        if (!containerId) {
            ({ container, services } = await client.createContainer(containerSchema));
            const id = await container.attach();
            location.hash = id;
        } else {
            try {
                ({ container, services } = await client.getContainer(containerId, containerSchema));
            } catch (e) {
                return;
            }
        }
        return services.audience;
    

Getting the audience on component mount

Now that we've defined how to get the Fluid audience, we need to tell React to call tryGetAudienceObject when the Audience Display component is mounted.

  1. Replace TODO 2 with the following code. Note that the user ID will come from the parent component as either user1 user2 or random. If the ID is random we use Math.random() to generate a random number as the ID. Additionally, a name will be mapped to the user based on their ID as specified in userNameList. Lastly, we define the state variables which will store the connected members as well as the current user. fluidMembers will store a list of all members connected to the container whereas currentMember will contain the member object representing the current user viewing the browser context.

        const userId = props.userId == "random" ? Math.random() : props.userId;
        const userNameList = {
        "user1" : "User One",
        "user2" : "User Two",
        "random" : "Random User"
        };
        const userName = userNameList[props.userId];
    
        const [fluidMembers, setFluidMembers] = useState();
        const [currentMember, setCurrentMember] = useState();
    
  2. Replace TODO 3 with the following code. This will call the tryGetAudienceObject when the component is mounted and set the returned audience members to fluidMembers and currentMember. Note, we check if an audience object is returned in case a user inputs a containerId which doesn't exist and we need to return them to the UserIdSelection view (props.onContainerNotFound() will handle switching the view). Also, it is good practice to deregister event handlers when the React component dismounts by returning audience.off.

        useEffect(() => {
        tryGetAudienceObject(userId, userName, props.containerId).then(audience => {
            if(!audience) {
            props.onContainerNotFound();
            alert("error: container id not found.");
            return;
            }
    
            const updateMembers = () => {
            setFluidMembers(audience.getMembers());
            setCurrentMember(audience.getMyself());
            }
    
            updateMembers();
    
            audience.on("membersChanged", updateMembers);
    
            return () => { audience.off("membersChanged", updateMembers) };
        });
        }, []);
    
  3. Replace TODO 4 with the following code. Note, if the fluidMembers or currentMember has not been initialized, a blank screen is rendered. The AudienceList component will render the member data with styling (to be implemented in the next section).

        if (!fluidMembers || !currentMember) return (<div/>);
    
        return (
            <AudienceList fluidMembers={fluidMembers} currentMember={currentMember}/>
        )
    

    Note

    Connection transitions can result in short timing windows where getMyself returns undefined. This is because the current client connection will not have been added to the audience yet, so a matching connection ID cannot be found. To prevent React from rendering a page with no audience members, we add a listener to call updateMembers on membersChanged. This works since the service audience emits a membersChanged event when the container is connected.

Create the view

  1. Replace TODO 5 with the following code. Note we are rendering a list component for each member passed from the AudienceDisplay component. For each member, we first compare member.userId to currentMember.userId to check if that member isSelf. This way, we can differentiate the client user from the other users and display the component with a different color. We then push the list component to a list array. Each component will display member data such as userId userName and additionalDetails.

        const currentMember = data.currentMember;
        const fluidMembers = data.fluidMembers;
    
        const list = [];
        fluidMembers.forEach((member, key) => {
            const isSelf = (member.userId === currentMember.userId);
            const outlineColor = isSelf ? 'blue' : 'black';
    
            list.push(
            <div style={{
                padding: '1rem',
                margin: '1rem',
                display: 'flex',
                outline: 'solid',
                flexDirection: 'column',
                maxWidth: '25%',
                outlineColor
            }} key={key}>
                <div style={{fontWeight: 'bold'}}>Name</div>
                <div>
                    {member.userName}
                </div>
                <div style={{fontWeight: 'bold'}}>ID</div>
                <div>
                    {member.userId}
                </div>
                <div style={{fontWeight: 'bold'}}>Connections</div>
                {
                    member.connections.map((data, key) => {
                        return (<div key={key}>{data.id}</div>);
                    })
                }
                <div style={{fontWeight: 'bold'}}>Additional Details</div>
                { JSON.stringify(member.additionalDetails, null, '\t') }
            </div>
            );
        });
    
  2. Replace TODO 6 with the following code. This will render all each of the member elements we pushed into the list array.

        return (
            <div>
                {list}
            </div>
        );
    

Setup UserIdSelection component

  1. Create and open a file \src\UserIdSelection.js in the code editor. This component will include user ID buttons and container ID input fields which allow end-users to choose their user ID and collaborative session. Add the following import statements and functional components:

    import { useState } from 'react';
    
    export const UserIdSelection = (props) => {
        // TODO 1: Define styles and handle user inputs
        return (
        // TODO 2: Return view components
        );
    }
    
  2. Replace TODO 1 with the following code. Note that the onSelectUser function will update the state variables in the parent App component and prompt a view change. The handleSubmit method is triggered by button elements which will be implemented in TODO 2. Also, the handleChange method is used to update the containerId state variable. This method will be called from an input element event listener implemented in TODO 2. Also, note that we update the containerId be getting the value from an HTML element with the id containerIdInput (defined in TODO 2).

        const selectionStyle = {
        marginTop: '2rem',
        marginRight: '2rem',
        width: '150px',
        height: '30px',
        };
    
        const [containerId, setContainerId] = (location.hash.substring(1));
    
        const handleSubmit = (userId) => {
        props.onSelectUser(userId, containerId);
        }
    
        const handleChange = () => {
        setContainerId(document.getElementById("containerIdInput").value);
        };
    
  3. Replace TODO 2 with the following code. This will render the user ID buttons as well as the container ID input field.

        <div style={{display: 'flex', flexDirection:'column'}}>
        <div style={{marginBottom: '2rem'}}>
            Enter Container Id:
            <input type="text" id="containerIdInput" value={containerId} onChange={() => handleChange()} style={{marginLeft: '2rem'}}></input>
        </div>
        {
            (containerId) ?
            (<div style={{}}>Select a User to join container ID: {containerId} as the user</div>)
            : (<div style={{}}>Select a User to create a new container and join as the selected user</div>)
        }
        <nav>
            <button type="submit" style={selectionStyle} onClick={() => handleSubmit("user1")}>User 1</button>
            <button type="submit" style={selectionStyle} onClick={() => handleSubmit("user2")}>User 2</button>
            <button type="submit" style={selectionStyle} onClick={() => handleSubmit("random")}>Random User</button>
        </nav>
        </div>
    

Start the Fluid server and run the application

Note

To match the rest of this how-to, this section uses npx and npm commands to start a Fluid server. However, the code in this article can also run against an Azure Fluid Relay server. For more information, see How to: Provision an Azure Fluid Relay service and How to: Connect to an Azure Fluid Relay service

In the Command Prompt, run the following command to start the Fluid service.

npx @fluidframework/azure-local-service@latest

Open a new Command Prompt and navigate to the root of the project; for example, C:/My Fluid Projects/fluid-audience-tutorial. Start the application server with the following command. The application opens in the browser. This may take a few minutes.

npm run start

Navigate to localhost:3000 on a browser tab to view the running application. To create a new container, select a user ID button while leaving the container ID input blank. To simulate a new user joining the container session, open a new browser tab and navigate to localhost:3000. This time, input the container ID value which can be found from first browser tab's url proceeding http://localhost:3000/#.

Note

You may need to install an additional dependency to make this demo compatible with Webpack 5. If you receive a compilation error related to a "buffer" or "url" package, please run npm install -D buffer url and try again. This will be resolved in a future release of Fluid Framework.

Next steps

  • Try extending the demo with more key/value pairs in the additionalDetails field in userConfig.
  • Consider integrating audience into a collaborative application which utilizes distributed data structures such as SharedMap or SharedString.
  • Learn more about Audience.

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.