# Ringing Call

Build custom incoming/outgoing call panels for [ring call workflow](https://getstream.io/video/docs/react/guides/joining-and-creating-calls/). The SDK includes a pre-built [`RingingCall`](https://getstream.io/video/docs/react/ui-components/call/ringing-call/) component, but this guide builds one from scratch.

## Best Practices

- Use `call.isCreatedByMe` to distinguish incoming vs outgoing calls.
- Show `VideoPreview` so users can check their camera before accepting.
- Disable accept/reject buttons during `CallingState.JOINING` to prevent double-clicks.
- Handle all calling states (`RINGING`, `JOINING`, `JOINED`) in your UI.

The component will render:

1. Call controls buttons to toggle audio and video enablement and accept resp. reject call buttons (the component will be called `CallControls`)
2. Avatars of called users or the current user's video preview (the component will be called `CallMembers`)
3. Call calling state indicator (the component will be called `CallCallingStateLabel`)

## App boilerplate

To start performing calls, we need a `StreamVideoClient` instance connected to the Stream's server network with app credentials. For more information on how to do that, please take a look at our [authentication guide](https://getstream.io/video/docs/react/guides/client-auth#basic-setup).

## Ringing call panel implementation

The component will rely on data accessed via our helper hooks:

- `useCall`
- `useCallCallingState`
- `useCallCreatedBy`
- `useCallMembers`

### User preview

The panel will render [`VideoPreview`](https://getstream.io/video/docs/react/ui-components/participants/video-preview/) so that the user can see the camera input or avatars if video preview is disabled. The avatar display logic is as follows:

- show all the called users' avatars if the call is outgoing (who am I calling)
- show the avatar of the person who initiated the call in case of an incoming call (who is calling me)

```tsx
import {
  useCall,
  useCallStateHooks,
  VideoPreview,
  UserResponse,
} from "@stream-io/video-react-sdk";

import { CallCallingStateLabel } from "./CallCallingStateLabel";
import { CallControls } from "./CallControls";
import { CallMembers } from "./CallMembers";
import { useEffect } from "react";

type RingingCallProps = {
  showMemberCount?: number;
};

export const CustomRingingCall = ({
  showMemberCount = 3,
}: RingingCallProps) => {
  const call = useCall();
  const { useCallMembers, useCallCreatedBy, useCameraState } =
    useCallStateHooks();

  const members = useCallMembers();
  const creator = useCallCreatedBy();

  const { camera, isMute: isCameraMute } = useCameraState();
  useEffect(() => {
    // enable the camera by default for all ring calls
    camera.enable();
  }, [camera]);

  if (!call) return null;

  const caller = creator;
  // show the caller if this is an incoming call or show all the users I am calling to
  let membersToShow: UserResponse[] = [];
  if (call.isCreatedByMe) {
    membersToShow =
      members
        ?.slice(0, showMemberCount)
        .map(({ user }) => user)
        .filter((u) => !!u) || [];
  } else if (caller) {
    membersToShow = [caller];
  }

  return (
    <div>
      {isCameraMute ? (
        <CallMembers members={membersToShow} />
      ) : (
        <VideoPreview />
      )}
      <CallCallingStateLabel />
      <CallControls />
    </div>
  );
};
```

#### Incoming call panel

In the snippet below, the `CallPanel` component is expected to render our `CustomRingingCall`.

```tsx
import {
  CallingState,
  StreamCall,
  useCall,
  useCallStateHooks,
  useCalls,
} from "@stream-io/video-react-sdk";

import { CustomActiveCallPanel } from "./CustomActiveCallPanel";
import { CustomRingingCall } from "./CustomRingingCall";

export const Video = () => {
  const calls = useCalls();
  return (
    <>
      {calls.map((call) => (
        <StreamCall call={call} key={call.cid}>
          <CallPanel />
        </StreamCall>
      ))}
    </>
  );
};

// custom component that renders ringing as well as active call UI
const CallPanel = () => {
  const call = useCall();
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();

  if (!call) return null;

  if (callingState === CallingState.JOINED) {
    return <CustomActiveCallPanel />;
  } else if (
    [CallingState.RINGING, CallingState.JOINING].includes(callingState)
  ) {
    return <CustomRingingCall />;
  }

  return null;
};
```

#### User avatars

We display relevant users' avatars with `CallMembers` component.

```tsx
import { Avatar } from "@stream-io/video-react-sdk";

import type { UserResponse } from "@stream-io/video-react-sdk";

type CallMembersProps = {
  members: UserResponse[];
};
const CallMembers = ({ members }: CallMembersProps) => {
  return (
    <div>
      {members.map((member) => (
        <div key={member.id}>
          <Avatar name={member.name} imageSrc={member.image} />
          {member.name && (
            <div>
              <span>{member.name}</span>
            </div>
          )}
        </div>
      ))}
    </div>
  );
};
```

### Call calling state label

To show our users the state of call connection, we implement `CallCallingStateLabel`. The calling state is provided by `useCallCallingState` hook:

<Admonition type="note">

In this component we are using translation service that comes bundled with the SDK. The service API is consumed with `useI18n` hook. Learn more about using the [i18n service in the dedicated documentation](https://getstream.io/video/docs/react/guides/localization/).

</Admonition>

```tsx
import {
  CallingState,
  useCallStateHooks,
  useI18n,
} from "@stream-io/video-react-sdk";

const CALLING_STATE_TO_LABEL: Record<CallingState, string> = {
  [CallingState.JOINING]: "Joining",
  [CallingState.RINGING]: "Ringing",
  [CallingState.RECONNECTING]: "Re-connecting",
  [CallingState.MIGRATING]: "Migrating",
  [CallingState.RECONNECTING_FAILED]: "Failed",
  [CallingState.OFFLINE]: "No internet connection",
  [CallingState.IDLE]: "",
  [CallingState.UNKNOWN]: "",
  [CallingState.JOINED]: "Joined",
  [CallingState.LEFT]: "Left call",
};

const CallCallingStateLabel = () => {
  const { t } = useI18n();
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();
  const callingStateLabel = CALLING_STATE_TO_LABEL[callingState];

  return callingStateLabel ? <div>{t(callingStateLabel)}</div> : null;
};
```

### Call controls

Finally, we build the call control buttons. We include buttons to toggle camera and microphone and buttons to accept and reject the call.

```tsx
import {
  AcceptCallButton,
  CallingState,
  CancelCallButton,
  IconButton,
  useCall,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";

const CallControls = () => {
  const call = useCall();
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();

  if (!call) return null;
  // show ringing call panel components only before the call is joined
  if (![CallingState.RINGING, CallingState.JOINING].includes(callingState))
    return null;

  // prevent users from rejecting call when already accepted
  const buttonsDisabled = callingState === CallingState.JOINING;

  return (
    <div>
      <ToggleAudioButton />
      <ToggleVideoButton />
      {call.isCreatedByMe ? (
        <CancelCallButton disabled={buttonsDisabled} />
      ) : (
        <>
          <AcceptCallButton disabled={buttonsDisabled} />
          <CancelCallButton
            onClick={() => {
              const reason = call.isCreatedByMe ? "cancel" : "decline";
              call.leave({ reject: true, reason });
            }}
            disabled={buttonsDisabled}
          />
        </>
      )}
    </div>
  );
};

const ToggleAudioButton = () => {
  const { useMicrophoneState } = useCallStateHooks();
  const { microphone, isMute } = useMicrophoneState();
  return (
    <IconButton
      icon={isMute ? "mic-off" : "mic"}
      onClick={() => microphone.toggle()}
    />
  );
};

const ToggleVideoButton = () => {
  const { useCameraState } = useCallStateHooks();
  const { camera, isMute } = useCameraState();
  return (
    <IconButton
      icon={isMute ? "camera-off" : "camera"}
      onClick={() => camera.toggle()}
    />
  );
};
```


---

This page was last updated at 2026-08-13T00:26:35.709Z.

For the most recent version of this documentation, visit [https://getstream.io/video/docs/react/ui-cookbook/ringing-call/](https://getstream.io/video/docs/react/ui-cookbook/ringing-call/).