State Layer

The SDK offers a reactive state store which lets you subscribe to relevant changes. This mechanism makes it very easy to know when the UI should be updated.

Client State

The client state stores the following values:

  • connected_user - the currently connected user through client.connectUser
  • is_ws_connection_healthy - whether the WS connection is healthy or not
import { FeedsClient } from "@stream-io/feeds-client";

const client = new FeedsClient("<API key>");
await client.connectUser({ id: "john" }, "<user token>");

// Subscribe to changes
const unsubscribe = client.state.subscribe((state) => {
  // It will return undefined if there is no connected user
  console.log(state.connected_user);
});

// Unsibscribe when you no longer need/want to receive updates
unsubscribe();

// Read current state
client.state.getLatestValue().connected_user;

Feed State

The feed state contains all information related to a feed (except for poll data, which is stored in poll state).

You can initialize feeds in two ways:

const feed = client.feed("user", "sara");
// Intialize feed state (will create feed, if it doesn't exist yet)
// Provide the watch flag to receive state updates via WebSocket events
await feed.getOrCreate({ watch: true });

// Query multiple feeds using a filter
const feeds = client.queryFeeds({
  filter: {
    // Your query
  },
  // Provide the watch flag to receive state updates via WebSocket events
  watch: true,
});

Feed state contains the following informations:

// Metadata of the feed
activity_count: number;
created_at: Date;
description: string;
feed: string;
follower_count: number;
following_count: number;
group_id: string;
id: string;
member_count: number;
name: string;
pin_count: number;
updated_at: Date;
created_by: UserResponse;
deleted_at?: Date;
visibility?: string;
filter_tags?: string[];
own_capabilities?: FeedOwnCapability[];
own_follows?: FollowResponse[];
custom?: Record<string, any>;
own_membership?: FeedMemberResponse;

// Feed content
activities?: ActivityResponse[];
aggregated_activities?: AggregatedActivityResponse[];
pinned_activities?: ActivityPinResponse[];
// Comments stored by activity/parent id
comments_by_entity_id?: Record<string, { comments: CommentsResponse[] }>

// Follows
followers?: FollowResponse[];
following?: FollowResponse[];
own_follows?: FollowResponse[]; // Do I follow this feed?

// Notification status
notification_status?: NotificationStatusResponse;

// Feed members
members?: FeedMemberResponse[];
own_membership?: FeedMemberResponse;

// Loading indicators
is_loading: boolean; // true when state initialization is in progress
is_loading_activities: boolean; // true when loading acitivities
followers_pagination: LoadingStates;
following_pagination: LoadingStates;

// `true` if the feed is receiving real-time updates via WebSocket
// Watched feeds are automatically refetched after WebSocket connection is dropped, and connection is restored
watch: boolean;

Once you have a feed instance, you can observe the state:

const unsubscribe = feed.state.subscribe((state) => {
  // Called everytime the state changes
  console.log(state);
});

// or if you only want to observe part of the state
const unsubscribe = feed.state.subscribeWithSelector(
  (state) => ({
    activities: state.activities,
  }),
  (state, prevState) => {
    console.log(state.activities, prevState?.activities);
  },
);

// Unsibscribe when you no longer need/want to receive updates
unsubscribe();

// Current state
console.log(feed.state.getLatestValue());

Poll State

Polls can exist with or without being attached to an activity.

One way to get a poll object is to create or query polls:

// Create a poll
const response = await client.createPoll({
  name: "Where should we host our next company event?",
  options: [{ text: "Amsterdam, The Netherlands" }, { text: "Boulder, CO" }],
});

// Or query polls
const response = await client.queryPolls({
  filter: {
    is_closed: true,
  },
  sort: [{ field: "created_at", direction: -1 }],
});

Or you’ll get a poll object by reading activities:

await feed.getOrCreate();

const poll = feed.state.getLatestValue().activities?.[0].poll;

If a poll is attached to an activity, you’ll get voting updates for that poll, if you’re watching the feed.

To receive these state updates you can use the pollFromState method:

await feed.getOrCreate();

// pollResponse object won't receive state updates
const pollResponse = feed.state.getLatestValue().activities?.[0].poll;

// poll object has a state store which can notify about state changes
const poll = client.pollFromState(pollResponse.id);

Once you have a poll instance, you can subscribe to changes:

const poll = client.pollFromState("<poll id>");

const unsubscribe = poll.state.subscribe((state) => {
  // Called everytime the state changes
  console.log(state);
});

// or if you only want to observe part of the state
const unsubscribe = poll.state.subscribeWithSelector(
  (state) => ({
    ownAnswer: state.own_answer,
  }),
  (state, prevState) => {
    console.log(state.ownAnswer, prevState?.ownAnswer);
  },
);

// Unsibscribe when you no longer need/want to receive updates
unsubscribe();

// Current state
console.log(poll.state.getLatestValue());

Poll state contains the following information:

allow_answers: boolean;
allow_user_suggested_options: boolean;
answers_count: number;
created_at: Date;
created_by_id: string;
description: string;
enforce_unique_vote: boolean;
name: string;
updated_at: Date;
vote_count: number;
latest_answers: PollVote[];
options: PollOption[];
custom: Record<string, any>;
latest_votes_by_option: Record<string, PollVote[]>;
vote_counts_by_option: Record<string, number>;
is_closed?: boolean;
max_votes_allowed?: number;
voting_visibility?: string;
created_by?: User;

// Enriched values
last_activity_at: Date;
max_voted_option_ids: OptionId[];
own_votes_by_option_id: Record<OptionId, PollVote>;
own_answer?: PollVote; // each user can have only one answer

Activity state

When creating an activity overview page, this is how you can create and initialize ActivityWithStateUpdates:

// When navigating to activity overview page
const activityWithStateUpdates = client.activityWithStateUpdates(activityId);
await activityWithStateUpdates.get({
  // Optionally fetch comments too
  comments: {
    limit: 10,
    depth: 2,
  },
});

// When leaving the page...
// dispose activity, this avoids refetching the activity if WebSocket reconnects
activityWithStateUpdates.dispose();

Once you have an ActivityWithStateUpdates instance, this is how you can subscribe to state updates:

const unsubscribe = activityWithStateUpdates.state.subscribe((state) => {
  // Called everytime the state changes
  console.log(state);
});
// or if you only want to observe part of the state
const unsubscribe = activityWithStateUpdates.state.subscribeWithSelector(
  (state) => ({
    activity: state.activity,
  }),
  (state, prevState) => {
    console.log(state.activity, prevState?.activity);
  },
);

// Unsibscribe when you no longer need/want to receive updates
unsubscribe();

// Current state
console.log(activityWithStateUpdates.state.getLatestValue());

This is how state looks like:

activity?: ActivityResponse;
// Comments stored by activity/parent id
comments_by_entity_id?: Record<string, { comments: CommentsResponse[] }>
// True when state is being fetched from API
is_loading: boolean;