---
source_url: https://www.pubnub.com/docs/sdks/javascript/quickstart-react
title: "Quickstart: PubNub JavaScript SDK in React"
updated_at: 2026-11-09T13:29:01.000Z
sdk_name: PubNub JavaScript SDK
sdk_version: 13.1.3
description: Connect a React app to PubNub with the JavaScript SDK. Subscribe in a useEffect hook, clean up on unmount, and publish a message.
---

# Quickstart: PubNub JavaScript SDK in React

PubNub JavaScript SDK, use the latest version: 13.1.3

Install:

```bash
npm install pubnub@13.1.3
```

> For AI agents: documentation index at https://www.pubnub.com/llms-full.txt

The JavaScript SDK works the same in a React app as in any other browser app. What changes is when your code runs. React mounts and unmounts components, and in development it mounts each one twice, so a subscription has to be created and removed with the component.

This quickstart builds a small component that subscribes to a channel, shows incoming messages, and publishes the text you type. It covers only what is specific to React. For the PubNub concepts behind each step, refer to [Quickstart: Web](https://www.pubnub.com/docs/sdks/javascript/quickstart-web.md).

## Before you begin

You need Node.js 22 or later, a React project (such as one created with [Vite](https://vitejs.dev/)), and a PubNub publish and subscribe key. To get your keys, follow [Get your PubNub keys](https://www.pubnub.com/docs/sdks/javascript/quickstart-web.md#get-your-pubnub-keys) in the Web quickstart.

## Install

:::note SDK version
Always use the latest SDK version to have access to the newest features and avoid security vulnerabilities, bugs, and performance issues.
:::

Install the SDK in your React project:

```bash
npm install pubnub
```

If you don't have a React project yet, create one first:

```bash
npm create vite@latest my-pubnub-app -- --template react-ts
cd my-pubnub-app
npm install
npm install pubnub
```

## Initialize

Import the SDK and create the client once, outside the component. Creating it inside the component would create a new client on every render.

```tsx
import { useEffect, useState } from 'react';
import PubNub from 'pubnub';

// Create the client once, outside the component, so re-renders reuse it
const pubnub = new PubNub({
  publishKey: 'YOUR_PUBLISH_KEY',
  subscribeKey: 'YOUR_SUBSCRIBE_KEY',
  userId: 'YOUR_USER_ID',
});
```

Replace the placeholders with your keys and a unique `userId`. Keep the `userId` unchanged for the lifetime of the user or the device. For more information, refer to the [Configuration](https://www.pubnub.com/docs/sdks/javascript/api-reference/configuration.md) section of the SDK documentation.

## Subscribe

Subscribe inside a `useEffect` hook and return a cleanup function that unsubscribes. React runs the cleanup when the component unmounts.

```tsx
const [messages, setMessages] = useState<string[]>([]);

useEffect(() => {
  const subscription = pubnub.channel('hello_world').subscription();

  subscription.onMessage = (event) => {
    setMessages((previous) => [...previous, `${event.publisher}: ${event.message}`]);
  };
  subscription.subscribe();

  // Runs on unmount, and before React re-runs the effect in development (StrictMode)
  return () => {
    subscription.unsubscribe();
  };
}, []);
```

The empty dependency array, `[]`, subscribes once per mount. The handler belongs to this subscription only, so unsubscribing also stops it.

:::note React StrictMode doubles effects in development
In development, StrictMode mounts each component, unmounts it, and mounts it again to expose missing cleanup. Without the cleanup function above, the first subscription stays active and every message appears twice. Production builds run effects once.
:::

For complete details on subscribing and handling events, refer to the [Subscribe](https://www.pubnub.com/docs/sdks/javascript/api-reference/subscribe.md#subscribe) section of the SDK documentation.

## Publish

Publishing is a plain async call, so it needs no special handling in React. This code publishes the text from the input box and clears it after a successful publish.

```tsx
const [text, setText] = useState('');

const send = async () => {
  if (!text.trim()) return;

  try {
    await pubnub.publish({ channel: 'hello_world', message: text });
    setText('');
  } catch (error) {
    console.error(`Publish failed: ${error}`);
  }
};
```

A message can be any JSON-serializable data that is smaller than 32 KiB. For more information, refer to the [Publish](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish.md#publish) section of the SDK documentation.

## Run

The component below combines all the previous steps. Save it as `src/App.tsx`, replace the placeholders, and start the app.

```tsx
import { useEffect, useState } from 'react';
import PubNub from 'pubnub';

// Create the client once, outside the component, so re-renders reuse it
const pubnub = new PubNub({
  publishKey: 'YOUR_PUBLISH_KEY',
  subscribeKey: 'YOUR_SUBSCRIBE_KEY',
  userId: 'YOUR_USER_ID',
});

export default function App() {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const subscription = pubnub.channel('hello_world').subscription();

    subscription.onMessage = (event) => {
      setMessages((previous) => [...previous, `${event.publisher}: ${event.message}`]);
    };
    subscription.subscribe();

    // Runs on unmount, and before React re-runs the effect in development (StrictMode)
    return () => {
      subscription.unsubscribe();
    };
  }, []);

  const [text, setText] = useState('');

  const send = async () => {
    if (!text.trim()) return;

    try {
      await pubnub.publish({ channel: 'hello_world', message: text });
      setText('');
    } catch (error) {
      console.error(`Publish failed: ${error}`);
    }
  };

  return (
    <div>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
      <input value={text} onChange={(event) => setText(event.target.value)} placeholder="Type a message" />
      <button onClick={send}>Send</button>
    </div>
  );
}
```

Verify that it works:

1. Start the development server with `npm run dev`.
2. Open [http://localhost:5173](http://localhost:5173) in your browser.
3. Type `Hello, world!` in the input box and click **Send**.
4. The message appears once in the list as `YOUR_USER_ID: Hello, world!`.
5. To see real-time communication, open the same URL in a second browser tab and send a message from either tab. It appears in both lists.

The component subscribes after it mounts, and it does not show when the subscription becomes active. If you click **Send** immediately after the page loads, the message can reach the channel before the subscription is active, and it does not appear in your list. Wait a moment, then send it again.

If the message does not arrive, refer to [Troubleshooting](https://www.pubnub.com/docs/sdks/javascript/troubleshooting.md). If the console shows a 403 (Access Denied) error, your keyset may have Access Manager enabled. Verify that your token has read permissions for the subscribed channel. Refer to [Configure access control](https://www.pubnub.com/docs/security/access-control/configure-access-control.md).

## Next steps

* Pick another path on [Choose your runtime](https://www.pubnub.com/docs/sdks/javascript/choose-runtime.md).
* Try [Presence](https://www.pubnub.com/docs/sdks/javascript/api-reference/presence.md) to track online and offline status.
* Implement [Message Persistence](https://www.pubnub.com/docs/sdks/javascript/api-reference/message-persistence.md) to store and retrieve messages.
* Use [Access Manager](https://www.pubnub.com/docs/sdks/javascript/api-reference/access-manager.md) to secure your channels.
* Building a chat app? Use the [Chat SDK](https://www.pubnub.com/docs/chat/chat-sdk.md).

Last updated at: 2026-11-09T13:29:01.000Z
