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 */;
};
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 throughclient.connectUser
andnull
otherwiseuseWsConnectionState()
- 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 optionsuseOwnCapabilities(feed)
- get permissions for currently connected user for that particular feeduseFollowers(feed)
- get a reactive list of followers of thefeed
in question (who/what follows thatfeed
)useFollowing(feed)
- get a reactive list of follows of thefeed
in question (who/what thatfeed
follows)useFeedActivities(feed)
- get a reactive list of activities for a feed and its pagination optionsuseFeedMetadata(feed)
- get often usedfeed
metadata for a givenfeed
useOwnFollows(feed)
- get a reactive list ofFollowResponse
s from your own feeds towards a givenfeed
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
.
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 anActivityResponse
or aCommentResponse
instance, depending on which level of depth of comments we are trying to usefeed
(optional) - aFeed
instance that can be optionally passed and used as a baseline to load the comments from (if omitted, the hook will try to use theFeed
instance from the nearestStreamFeedContext
)
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 FollowResponse
s, 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 ofFeedsClient
and gracefully connecting a user to ituseReactionActions({ 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 apptokenOrProvider
- can be either an already available usertoken
or atokenProvider
function that returns a user tokenuserData
- an instance ofUserRequest
options
- an instance ofFeedsClientOptions
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 anActivityResponse
or aCommentResponse
instancetype
- the type of reaction we want to add or remove
It returns 3 helper methods:
addReaction
- adds a reaction of the specifiedtype
to the givenentity
removeReaction
- removes a reaction of a specifictype
from the givenentity
toggleReaction
- will runaddReaction
if we do not have a reaction of the providedtype
andremoveReaction
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.Context
s 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.
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.