This guide will help you get started with River’s React SDK. You’ll learn how to:

  1. Install the React SDK
  2. Set up the necessary providers
  3. Connect to River
  4. Create a space
  5. Send your first message

Installation

You can start a new River project in two ways:

The easiest way to get started is using create-river-build-app. This will set up a new React project with all the necessary dependencies and configurations.

# npm
npm create river-build-app my-app

# yarn
yarn create river-build-app my-app

# pnpm
pnpm create river-build-app my-app

By default, this will create a new app using the River Playground template - a full-featured example application that demonstrates River’s capabilities.

Manual Installation

If you prefer to add the React SDK to an existing project, install the required dependencies:

yarn add @river-build/react-sdk @river-build/sdk vite-plugin-node-polyfills

Then, add the vite-plugin-node-polyfills to your vite.config.ts:

vite.config.ts
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  // ...rest of your config
  plugins: [
    // ...rest of your plugins
    nodePolyfills(),
    react(),
  ],
})

Setting Up Providers

River requires a few providers to be set up at the root of your application:

  1. WagmiProvider - For Web3 wallet connection (recommended)
  2. RiverSyncProvider - For interacting with River Network

Here’s how to set them up:

App.tsx
import { RiverSyncProvider } from "@river-build/react-sdk";
import { base, baseSepolia } from 'viem/chains'
import { WagmiConfig, createConfig } from "wagmi";
import { createPublicClient, http } from 'viem'

// Configure Wagmi
export const wagmiConfig = createConfig({
  autoConnect: true,
  publicClient: createPublicClient({
    chain: [base, baseSepolia],
    transport: http()
  }),
})

function App() {
  return (
    <WagmiConfig config={wagmiConfig}>
      <RiverSyncProvider>
        {/* Your app content */}
      </RiverSyncProvider>
    </WagmiConfig>
  );
}

If you need any assistance with setting up Wagmi, check out the Wagmi Getting Started Guide.

Connecting to River

You can connect to River using either a Web3 wallet (recommended) or a bearer token.

River has three networks:

  • omega - The River mainnet, uses Base as the EVM chain
  • gamma - The River testnet, uses Base Sepolia as the EVM chain
  • alpha - The River testnet, uses Base Sepolia as the EVM chain

If you’re not sure which network to use, alpha is a good starting point.

You can use other alpha clients like Towns (alpha) or the River Playground to help you inspect messages on the alpha network.

Using a Web3 Wallet

import { useAgentConnection } from "@river-build/react-sdk";
import { makeRiverConfig } from "@river-build/sdk";
import { getEthersSigner } from "./utils/viem-to-ethers";
import { wagmiConfig } from "./config/wagmi";

const riverConfig = makeRiverConfig("alpha");

function ConnectButton() {
  const { connect, isAgentConnecting, isAgentConnected } = useAgentConnection();

  return (
    <button
      onClick={async () => {
        const signer = await getEthersSigner(wagmiConfig);
        connect(signer, { riverConfig });
      }}
    >
      {isAgentConnecting ? "Connecting..." : "Connect"}
      {isAgentConnected && "Connected ✅"}
    </button>
  );
}

You’ll need to use getEthersSigner to get the signer from viem wallet client. River SDK uses ethers under the hood, so you’ll need to convert the viem wallet client to an ethers signer.

You can get the getEthersSigner function from Wagmi docs.

Using a Bearer Token

You can connect to River using a pre-existing identity. This allows you to read and send messages, but you won’t be able to create spaces or channels (on-chain actions).

If you have a Towns account, you can get your bearer token from there. Type /bearer-token in any conversation to get your token.

import { useAgentConnection } from "@river-build/react-sdk";
import { makeRiverConfig } from "@river-build/sdk";

const riverConfig = makeRiverConfig("alpha");

function ConnectButton() {
  const { connectUsingBearerToken, isAgentConnecting, isAgentConnected } = useAgentConnection();
  const [bearerToken, setBearerToken] = useState('');

  return (
    <div>
      <input 
        value={bearerToken} 
        onChange={(e) => setBearerToken(e.target.value)}
        placeholder="Enter bearer token" 
      />
      <button 
        onClick={() => connectUsingBearerToken(bearerToken, { riverConfig })}
      >
        {isAgentConnecting ? "Connecting..." : "Connect"}
        {isAgentConnected && "Connected ✅"}
      </button>
    </div>
  );
}

Creating a Space

Once connected, you can start interacting with River. Here’s how to create a space:

You can only create a space with a Web3 wallet. If you’re using a bearer token, you can’t create spaces.

import { useCreateSpace } from "@river-build/react-sdk";
import { getEthersSigner } from "@/utils/viem-to-ethers";
import { wagmiConfig } from "@/config/wagmi";

function CreateSpace({ onCreateSpace }: { onCreateSpace: (spaceId: string) => void }) {
  const { createSpace, isPending } = useCreateSpace();
  const [spaceName, setSpaceName] = useState('');

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const signer = await getEthersSigner(wagmiConfig);
        const { spaceId } = await createSpace({ spaceName }, signer);
        // Let a parent component handle the spaceId, keep reading the guide! 
        onCreateSpace(spaceId);
        // Reset form
        setSpaceName('');
      }}
    >
      <div>
        <input
          value={spaceName}
          onChange={(e) => setSpaceName(e.target.value)}
          placeholder="Space name"
          required
        />
      </div>
      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create Space"}
      </button>
    </form>
  );
}

Sending Your First Message

With the space created, we can send a message to the #general channel. Here’s how to create a form

import {
  useChannel,
  useSendMessage,
  useSpace,
  useTimeline,
  useUserSpaces,
} from '@river-build/react-sdk'
import { RiverTimelineEvent } from '@river-build/sdk'
import { useState } from 'react'

// Form to send a message to a channel
function ChatInput({ spaceId, channelId }: { spaceId: string; channelId: string }) {
  const { sendMessage, isPending } = useSendMessage(channelId)
  const { data: channel } = useChannel(spaceId, channelId)
  const [message, setMessage] = useState('')

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        sendMessage(message)
        setMessage('')
      }}
    >
      <input
        value={message}
        placeholder={`Type a message in ${channel?.metadata?.name || 'unnamed channel'}`}
        onChange={(e) => setMessage(e.target.value)}
      />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
    </form>
  )
}

// Display messages from a channel
function ChatMessages({ channelId }: { channelId: string }) {
  const { data: events } = useTimeline(channelId)
  return (
    <div>
      {/* Filter out non-message events */}
      {events
        ?.filter((event) => event.content?.kind === RiverTimelineEvent.RoomMessage)
        .map((message) => (
          <p key={message.eventId}>
            {message.content?.kind === RiverTimelineEvent.RoomMessage
              ? message.content?.body
              : ''}
          </p>
        ))}
    </div>
  )
}

// Chat component that renders the chat input and messages, given a spaceId
function Chat({ spaceId }: { spaceId: string }) {
  const { data: space } = useSpace(spaceId)
  const channelId = space?.channelIds?.[0] // Spaces have a default channel called `general`

  return (
    <>
      {channelId && <ChatInput spaceId={spaceId} channelId={channelId} />}
      {channelId && <ChatMessages channelId={channelId} />}
    </>
  )
}

// Renders the name of a space
const SpaceName = ({ spaceId }: { spaceId: string }) => {
  const { data: space } = useSpace(spaceId)
  return <>{space?.metadata?.name}</>
}

// Chat app that renders the create space and chat components
export const ChatApp = () => {
  // Get the list of spaces we're a member of
  const { spaceIds } = useUserSpaces()
  const [spaceId, setSpaceId] = useState('')

  return (
    <div>
      {/* Show a list of spaces we're a member of, click to select one */}
      {spaceIds.map((spaceId) => (
        <button type="button" key={spaceId} onClick={() => setSpaceId(spaceId)}>
          <SpaceName spaceId={spaceId} />
        </button>
      ))}
      {/* Show the create space form if we don't have a space */}
      {!spaceId && <CreateSpace onCreateSpace={(spaceId) => setSpaceId(spaceId)} />}
      {/* Render the chat component with the current selected space */}
      {spaceId && (
        <h1>
          Chatting in <SpaceName spaceId={spaceId} />
        </h1>
      )}
      {spaceId && <Chat spaceId={spaceId} />}
    </div>
  )
}

Add the ChatApp component to your app and you’re ready to start chatting!

Next Steps

Now that you have the basics set up, you can:

Check out our Playground template for a full-featured example application.