Activity Feeds V3 is in closed alpha — do not use it in production (just yet).

React Bindings

The JS client follows a framework-agnostic reactive state management pattern. If you’re creating a React application you’ll probably want to access the reactive state using hooks or passing them through context providers; the SDK (@stream-io/feeds-client/react-bindings) comes with a few quality-of-life state and utility hooks for you to use within your application.

@stream-io/feeds-client works with or without any JavaScript framework. If you’re not building a React application, you don’t have to import anything from @stream-io/feeds-client/react-bindings, nor will any part of the library end up in your application bundle.

React State Hooks

The state hooks are essentially wrappers around reactive state properties, making their usage trivial. Namely, these can be divided into the following groups:

  • Client state hooks
    • useClientConnectedUser() - get the user that has been connected through client.connectUser and null otherwise
    • useWsConnectionState() - get the websocket connection state for the currently connected user
  • Feed state hooks
    • useComments(feed, entity) - get reactive comment lists of any parent and their pagination options
    • useOwnCapabilities(feed) - get permissions for currently connected user for that particular feed
    • useFollowers(feed) - get a reactive list of followers of the feed in question (who/what follows that feed)
    • useFollowing(feed) - get a reactive list of follows of the feed in question (who/what that feed follows)
    • useFeedActivities(feed) - get a reactive list of activities for a feed and its pagination options
    • useFeedMetadata(feed) - get often used feed metadata for a given feed
    • useOwnFollows(feed) - get a reactive list of FollowResponses from your own feeds towards a given feed

and finally useStateStore from which all of the previously mentioned state hooks are derived.

useClientConnectedUser

In order to not have to wait for user connection to finish through client.connectUser, we’ve introduced a reactive property of the client state called connectedUser. The purpose of this hook is to return that specific reactive value that can then be used to determine how we render the UI and which parts are supposed to be placeholders until the user connects, rather than blocking the entire UI.

If connection has not finished yet, it will return null.

import { useClientConnectedUser } from "@stream-io/feeds-client/react-bindings";

const FeedComponent = () => {
  const connectedUser = useClientConnectedUser();

  if (!connectedUser) {
    return /* Render some placeholder */;
  }

  return /* Render the actual component tree */;
};

Note that it is important that the useClientConnectedUser hook is used within the StreamFeedsContext or within the StreamFeeds wrapper. It will simply return null otherwise.

useWsConnectionState

It is occasionally the case that due to unstable network conditions, complete loss of network or other circumstances the websocket connection is broken. This hook returns an object containing information about the current state of the websocket connection.

import { useWsConnectionState } from "@stream-io/feeds-client/react-bindings";

const FeedComponent = () => {
  const { is_healthy: isHealthy } = useWsConnectionState();

  return (
    <>
      {isHealthy ? null : <ConnectionLostBanner />}
      <ActualContent />
    </>
  );
};

Note that it is important that the useWsConnectionState hook is used within the StreamFeedsContext or within the StreamFeeds wrapper. It will simply return { isHealthy: false } otherwise.

useComments

Comments in feeds can nest multiple levels deep so to simplify working with the state structure we chose to keep things fast, we’ve introduced useComments hook which provides the comments, pagination as well as some pagination utility methods.

The hook accepts an object with 2 properties:

  • parent - either an ActivityResponse or a CommentResponse instance, depending on which level of depth of comments we are trying to use
  • feed (optional) - a Feed instance that can be optionally passed and used as a baseline to load the comments from (if omitted, the hook will try to use the Feed instance from the nearest StreamFeedContext)

The hook will internally determine what type of parent you are passing to it and provide the correct APIs accordingly.

import type { CommentParent, Feed } from "@stream-io/feeds-client";
import { useComments } from "@stream-io/feeds-client/react-bindings";

const Comments = ({
  commentOrActivity,
  feed,
}: {
  commentOrActivity: CommentParent;
  feed: Feed;
}) => {
  const {
    comments,
    comment_pagination,
    has_next_page,
    is_loading_next_page,
    loadNextPage,
  } = useComments({ feed, parent: commentOrActivity });

  // or `useComments({ parent: commentOrActivity })` if the component is wrapped within a `StreamFeedContext`

  return <div>...</div>;
};

To load more comments (or initial pages) you can simply call the loadNextPage method. The hook will then pull the data from the state store based on the commentOrActivity.id.

Since the feed prop is optional, you can either pass it to the useComments hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useFollowers

Having followers is a very popular usecase for activity feeds. To facilitate this, we’ve introduced a useFollowers hook that provides a list of followers, pagination as well as some pagination utility methods.

import type { Feed } from "@stream-io/feeds-client";
import { useFollowers } from "@stream-io/feeds-client/react-bindings";

const Followers = ({ feed }: { feed: Feed }) => {
  const {
    followers,
    follower_count,
    followers_pagination,
    is_loading_next_page,
    has_next_page,
    loadNextPage,
  } = useFollowers(feed);

  // or `useFollowers()` if the component is wrapped within a `StreamFeedContext`

  return <div>...</div>;
};

To load more followers (or initial pages) you can simply call the loadNextPage method. The hook will then pull the data from the state store based on the feed.

Since the feed argument is optional, you can either pass it to the useFollowers hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useFollowing

Similar to having followers, following users is also a very popular usecase for activity feeds. To facilitate this, we’ve introduced a useFollowing hook that provides a list of users we follow, pagination as well as some pagination utility methods.

import type { Feed } from "@stream-io/feeds-client";
import { useFollowing } from "@stream-io/feeds-client/react-bindings";

const Following = ({ feed }: { feed: Feed }) => {
  const {
    following,
    following_count,
    following_pagination,
    is_loading_next_page,
    has_next_page,
    loadNextPage,
  } = useFollowing(feed);

  // or `useFollowing()` if the component is wrapped within a `StreamFeedContext`

  return <div>...</div>;
};

To load more users you follow (or initial pages) you can simply call the loadNextPage method. The hook will then pull the data from the state store based on the feed.

Since the feed argument is optional, you can either pass it to the useFollowing hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useFeedActivities

Activities are the core atomic building blocks of an activity feed. For ease of use, we’ve introduced a useFeedActivities hook that provides a list of activities for a feed, pagination as well as some pagination utility methods.

import type { Feed } from "@stream-io/feeds-client";
import { useFeedActivities } from "@stream-io/feeds-client/react-bindings";

const Activities = ({ feed }: { feed: Feed }) => {
  const { activities, isLoading, hasNextPage, loadNextPage } =
    useFeedActivities(feed) ?? {};

  // or `useFeedActivities()` if the component is wrapped within a `StreamFeedContext`

  return <div>...</div>;
};

To load more activities (or initial pages) you can simply call the loadNextPage method. The hook will then pull the data from the state store based on the feed.

Since the feed argument is optional, you can either pass it to the useFeedActivities hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useFeedMetadata

Whenever displaying a feed (either as a link towards someone’s profile screen, or perhaps just as a list of activities some specific entity holds) it is often useful to also display a variety of metadata; typically related to the number of followers/following, dates and so on. For this, we provide the useFeedMetadata hook which will return exactly that.

import type { Feed } from "@stream-io/feeds-client";
import { useFeedMetadata } from "@stream-io/feeds-client/react-bindings";

const ProfileScreen = ({ feed }: { feed: Feed }) => {
  const {
    created_by,
    follower_count,
    following_count,
    created_at,
    updated_at,
  } = useFeedMetadata(feed) ?? {};

  // or `useFeedMetadata()` if the component is wrapped within a `StreamFeedContext`

  return <div>...</div>;
};

Since the feed argument is optional, you can either pass it to the useFeedMetadata hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useOwnFollows

Whenever examining a specific feed, it is often useful to know if we have any feeds that we have created that follow it specifically. This is the main way typically followers and followings are considered and accounted for.

For this purpose, we have the own_follows state field which essentially gives us an array of FollowResponses, where the source_feed property is going to be one of the feeds we’ve created and target_feed will be the feed we are examining.

A common common usecase is to have a specific feed.groupId that usually does the following (for example a timeline feed) and this information can then be leveraged to determine whether we are truly following the provided feed or not.

import type { Feed } from "@stream-io/feeds-client";
import { useOwnFollows } from "@stream-io/feeds-client/react-bindings";

const Follows = ({ feed }: { feed: Feed }) => {
  const { own_follows } = useOwnFollows(feed) ?? {};

  // or `useOwnFollows()` if the component is wrapped within a `StreamFeedContext`

  const ownFollow = useMemo(
    () =>
      ownFollows &&
      ownFollows.find(
        (follow: FollowResponse) => follow.source_feed.group_id === "timeline",
      ),
    [ownFollows],
  );

  const followStatus = ownFollow.status;

  if (followStatus === "accepted") {
    console.log(`I am following feed ${feed.id} !`);
  }

  if (followStatus === "pending") {
    console.log(`I have requested to follow ${feed.id} !`);
  }

  if (followStatus === "rejected") {
    console.log(`Feed ${feed.id} has rejected my follow request !`);
  }

  return <div>...</div>;
};

Since the feed argument is optional, you can either pass it to the useOwnFollows hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useOwnCapabilities

Permissions are important and they can change throughout the application lifetime, it’s better to hide certain UI parts or disable certain inputs if current user does not have access to them.

import type { Feed } from "@stream-io/feeds-client";
import { useOwnCapabilities } from "@stream-io/feeds-client/react-bindings";

const FeedComponent = ({ feed }: { feed: Feed }) => {
  const { can_read_feed: canReadFeed } = useOwnCapabilities(feed);
  // or `useOwnCapabilities()` if the component is wrapped within a `StreamFeedContext`

  if (!canReadFeed) {
    return <div>You don't have permission to read this feed</div>;
  }

  return <div id="feed">...</div>;
};

Note that it’s important that feed that’s being passed to the useOwnCapabilities has been properly initialized through Feed.getOrCreate method. If the feed wasn’t initialized, all of the permissions returned from the hook will default to false.

Since the feed argument is optional, you can either pass it to the useOwnCapabilities hook or you can make sure the hook is used within the StreamFeedContext or within the StreamFeed wrapper. It is going to fetch the Feed instance from the context if it is not passed.

useStateStore

This hook is something we call a “connector” or a “binding” between our StateStore API and a React library. If there’s not an SDK-provided hook that returns a specific information when it comes to feed state, you can easily craft your own.

Here’s the simplest useStateStore hook usage example:

import type { Feed, FeedState } from "@stream-io/feeds-client";
import { useStateStore } from "@stream-io/feeds-client/react-bindings";

const selector = (currentState: FeedState) => ({
  members: currentState.members,
});

const Component = ({ feed }: { feed: Feed }) => {
  const { members } = useStateStore(feed.state, selector);

  return (
    <>
      {members.map((member) => (
        <div key={member.user.id}>{member.user.name}</div>
      ))}
    </>
  );
};

In this example the component will re-render each time members (and only members) property changes always displaying most up-to-date UI.

You can extract selector, binding and transformations to a custom hook module and use it across your application.

React Utility Hooks

Within the @stream-io/feeds-client/react-bindings package we also provide some utility hooks that are meant to make some common pain points easier to integrate.

The currently available utility hooks are:

  • useCreateFeedsClient({ apiKey, tokenOrProvider, userData, options }) - takes care of the creation of an instance of FeedsClient and gracefully connecting a user to it
  • useReactionActions({ entity, type }) - provides various helper methods for adding/removing reactions for both comments and activities

useCreateFeedsClient

One of the most important things to get right in a brand new integration is how and when a client is created and how we connect a user to it. For that purpose, we expose a hook that should make this relatively trivial.

It takes the following properties:

  • apiKey - the API key of your app
  • tokenOrProvider - can be either an already available user token or a tokenProvider function that returns a user token
  • userData - an instance of UserRequest
  • options - an instance of FeedsClientOptions
import type { UserRequest } from "@stream-io/feeds-client";
import {
  useCreateFeedsClient,
  StreamFeeds,
} from "@stream-io/feeds-client/react-bindings";

const apiKey = "123";
const tokenProvider = async (userId: string) => {
  const token = await fetchTokenFromSomewhere();
  return token;
};

const options = { timeout: 10000 };

const FeedComponent = (user: UserRequest) => {
  const tokenOrProvider = useCallback(async () => {
    return tokenProvider(user.id);
  }, [user.id]);

  // or alternatively, if we've already computed the token elsewhere
  // const tokenOrProvider = useMemo(() => user.token, [user.token]);

  const client = useCreateFeedsClient({
    apiKey,
    tokenOrProvider,
    userData,
    options,
  });

  if (!client) {
    return null;
  }

  return <StreamFeeds client={client}>{/* your content here */}</StreamFeeds>;
};

If the invocation of client.connectUser promise rejects, an error will be thrown by the hook. This is done by design as these errors are irrecoverable on our side and it is the responsibility of integrators to handle these errors however they see fit.

Popular options are ErrorBoundary wrappers or perhaps custom error managers that either handle some fallback UI or let the user react to these errors.

useReactionActions

Sending and removing reactions can be done to both activities and comments, each being similar but accepting different props. For this purpose, we provide a utility hook in the SDK that helps with this significantly.

It takes the following properties:

  • entity - can either be an ActivityResponse or a CommentResponse instance
  • type - the type of reaction we want to add or remove

It returns 3 helper methods:

  • addReaction - adds a reaction of the specified type to the given entity
  • removeReaction - removes a reaction of a specific type from the given entity
  • toggleReaction - will run addReaction if we do not have a reaction of the provided type and removeReaction otherwise
export const Reaction = ({
  type,
  entity,
}: {
  type: string;
  entity: ActivityResponse | CommentResponse;
}) => {
  const { addReaction, removeReaction, toggleReaction } = useReactionActions({
    entity,
    type,
  });

  return (
    <>
      <button onClick={toggleReaction}>{/* your content here */}</button>
      <button onClick={addReaction}>{/* your content here */}</button>
      <button onClick={removeReaction}>{/* your content here */}</button>
    </>
  );
};

Note that it is important that the useReactionActions hook is used within the StreamFeedsContext or within the StreamFeeds wrapper.

React Contexts and Wrappers

The @stream-io/feeds-client/react-bindings also exposes some useful React.Contexts as well as contextual hooks that can be used for ease of data flow and by underlying components as well.

StreamFeedsContext

The StreamFeedsContext is the top-most context that should be applied within one’s integration.

For its value it passes an instance of FeedsClient, which is also what its Provider expects.

It comes with a utility wrapper StreamFeeds that expects a single prop - which is client.

import type { FeedsClient } from "@stream-io/feeds-client";
import { StreamFeeds } from "@stream-io/feeds-client/react-bindings";

const FeedsWrapperComponent = (client: FeedsClient) => {
  return <StreamFeeds client={client}>{/* your content here */}</StreamFeeds>;
};

Anywhere within the wrapper, you can then use the useFeedsClient hook to get a hold of the value passed to StreamFeeds.

The StreamFeeds wrapper component is expected to wrap all content related to feeds in your app.

You do not need to wrap your entire app with it, but rather simply make sure that all places using anything from our react-bindings package are wrapped within, as it is very likely that they’ll depend on the useFeedsClient hook.

StreamFeedContext

The StreamFeedContext is a context expected to wrap components that utilize a Feed instance heavily, for much easier use. It will also make all of the feed state hooks no longer require you to pass a feed to them (although you can still do so).

For its value it passes an instance of Feed, which is also what its Provider expects.

It comes with a utility wrapper StreamFeed that expects a single prop - which is feed.

import type { Feed } from "@stream-io/feeds-client";
import { StreamFeed } from "@stream-io/feeds-client/react-bindings";

const FeedWrapperComponent = (feed: Feed) => {
  return <StreamFeed feed={feed}>{/* your content here */}</StreamFeed>;
};

Anywhere within the wrapper, you can then use the useFeedContext hook to get a hold of the value passed to StreamFeed.

The StreamFeed wrapper component is useful when it wraps all content related to a certain feed in your app, if possible.

You do not need to wrap your entire app with it.

Selector Rules

Stability

Make sure your selector is either defined outside your component’s scope or is memoized through the use of useMemo or useCallback if it relies on some outside property before passing it down to the useStateStore hook. The instability of the selector has negative implications on your application’s performance.

import type {
  Feed,
  FeedState,
  ActivityResponse,
  CommentResponse,
} from "@stream-io/feeds-client";
import { useStateStore } from "@stream-io/feeds-client/react-bindings";

const Component = ({
  feed,
  entity,
}: {
  feed: Feed;
  entity: ActivityResponse | CommentResponse;
}) => {
  const id = entity.id;

  // don't do this 👎
  const selector = (currentState: FeedState) => ({
    comments: currentState.comments_by_entity_id[id].comments ?? [],
  });

  // do this instead 👍
  const selector = useCallback(
    (currentState: FeedState) => ({
      comments: currentState.comments_by_entity_id[id].comments ?? [],
    }),
    [id],
  );

  const { comments } = useStateStore(feed.state, selector);

  return (
    <>
      {members.map((member) => (
        <div key={member.user.id}>{member.user.name}</div>
      ))}
    </>
  );
};

Note that this is just an example, comments in feeds can nest many levels deep and to make work with them easy the SDK already comes with a useComments hook which does exactly the same thing under the hood.

Selector Output

Selectors run each time the state changes so the top-level properties of the selector’s output should be easily comparable between state changes (using Object.is()), if you recreate complex values between selections, the component using this selector will re-render each time the state changes.

Selectors are only for pulling specific parts of the state from the “central” object, if you need transformations, do it in a separate step instead.

Note that adding or removing keys dynamically between selections is not supported, make sure the amount of keys and keys itself stay the same between selections.

useMembersById.ts
import type { Feed, FeedState } from "@stream-io/feeds-client";
import { useStateStore } from "@stream-io/feeds-client/react-bindings";

// don't do this 👎
const selector = ({ members }: FeedState) => ({
  membersById: members.reduce((newMembersById, member) => {
    newMembersById[member.user.id] = member;
    return newMembersById;
  }, {}),
});

const useMembersById = (feed: Feed) => {
  const { membersById } = useStateStore(feed.state, selector);

  return membersById;
};

// do this instead 👍
const selector = ({ members }: FeedState) => ({
  members,
});

const useMembersById = (feed: Feed) => {
  const { members } = useStateStore(feed.state, selector);

  return useMemo(
    () =>
      members.reduce((newMembersById, member) => {
        newMembersById[member.user.id] = member;
        return newMembersById;
      }, {}),
    [members],
  );
};

It is possible to do simple transformations within the selectors as long as the top level keys hold primitive values which are easy to compare between selections.

© Getstream.io, Inc. All Rights Reserved.