Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/unit-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ runs:
run: |
npm run test:unit
shell: bash
- name: Run type tests
run: |
npm run test:types
shell: bash
7 changes: 7 additions & 0 deletions doc/7/getting-started/react/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
code: false
type: branch
title: React
description: Get started with the Javascript SDK and React
order: 200
---
106 changes: 106 additions & 0 deletions doc/7/getting-started/react/standalone/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
code: false
type: page
title: Standalone
description: Getting started with Kuzzle and React
order: 100
---

# Getting Started with Kuzzle and React

This tutorial explains how to use **Kuzzle** with the **Javascript SDK 7** and **React**.

You are going to write a realtime chat: messages are stored as **documents** in Kuzzle, and every client is kept up to date through [document notifications](/sdk/js/7/essentials/realtime-notifications#document-messages).

To follow this tutorial, you must have a Kuzzle Server up and running. Follow these instructions if this is not already the case: [Running Kuzzle](/core/2/guides/getting-started/run-kuzzle).

:::info
Having trouble? Get in touch with us on [Discord](http://join.discord.kuzzle.io)!
:::

## Requirements

- **Node.js** >= 20 ([download page](https://nodejs.org/en/download/))
- a **running Kuzzle V2 stack** ([instructions here](/core/2/guides/getting-started/run-kuzzle))

## Prepare your environment

Create a React application with [Vite](https://vite.dev/) and install the Javascript SDK:

```bash
npm create vite@latest kuzzle-playground -- --template react
cd kuzzle-playground
npm install
npm install kuzzle-sdk@7
```

You can now empty `src/App.jsx` and `src/App.css`: we are going to rewrite them.

## Instantiating the SDK

The SDK client holds the network connection, so the whole application must share a single instance.

Create a `src/services/kuzzle.js` file:

<<< ./snippets/kuzzle.js

:::info
Replace `localhost` with the hostname of the machine running your Kuzzle server.
:::

## Connecting and loading the messages

Everything that talks to Kuzzle lives in a single custom hook. Create a `src/useChat.js` file:

<<< ./snippets/useChat.js

This hook does the following, once the user has chosen a nickname:

- [connects](/sdk/js/7/core-classes/kuzzle/connect) the SDK to Kuzzle,
- creates the `chat` [index](/sdk/js/7/controllers/index/create) and the `messages` [collection](/sdk/js/7/controllers/collection/create) if they don't [exist](/sdk/js/7/controllers/index/exists) yet,
- [subscribes](/sdk/js/7/controllers/realtime/subscribe) to the collection, so that every message created by any client is prepended to the local state,
- [searches](/sdk/js/7/controllers/document/search) for the hundred most recent messages to fill the history,
- exposes a `sendMessage` function that [creates](/sdk/js/7/controllers/document/create) a new document.

:::info
The subscription is opened **before** the history is fetched, and the cleanup function [unsubscribes](/sdk/js/7/controllers/realtime/unsubscribe) when the component unmounts. This way no message can slip through between the two calls, and React's Strict Mode does not leave a dangling room behind.
:::

Note that `sendMessage` does not touch the local state: the new message comes back through the realtime notification, exactly like the ones sent by the other clients.

## Displaying the messages

Create a `src/Message.jsx` component to render a single message:

<<< ./snippets/Message.jsx

Then add the styles in `src/App.css`:

<<< ./snippets/App.css

## Putting it together

Finally, rewrite `src/App.jsx`. It asks for a nickname, then displays the message list and the input used to send new ones:

<<< ./snippets/App.jsx

Launch the application:

```bash
npm run dev
```

:::success
Open the printed URL in two different browser tabs, pick a different nickname in each one, and send a message: it shows up in both tabs instantly, pushed by Kuzzle.
:::

## Going further

Now that you are more familiar with Kuzzle, dive even deeper to learn how to leverage its full capabilities:

- Follow the [React with Redux](/sdk/js/7/getting-started/react/with-redux) tutorial to move this state into a Redux store
- Discover what this SDK has to offer by browsing other sections of this documentation
- Learn more about Kuzzle [realtime engine](/core/2/guides/main-concepts/realtime-engine)
- Learn how to use the Kuzzle [Admin Console](http://console.kuzzle.io) to manage your users and data
- Learn how to use [Koncorde](/core/2/api/koncorde-filters-syntax) to create incredibly fine-grained and blazing-fast subscriptions
- Follow our guide to learn how to [manage users, and how to set up fine-grained access control](/core/2/guides/main-concepts/permissions)
28 changes: 28 additions & 0 deletions doc/7/getting-started/react/standalone/snippets/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
}

.messages {
padding: 10px;
margin: 10px;
width: 45vw;
border-radius: 10px;
}

.fromMe {
text-align: right;
float: right;
margin-left: 49vw;
background-color: rgb(0 40 53 / 30%);
}

.fromOthers {
text-align: left;
float: left;
margin-right: 49vw;
color: #eeefff;
background-color: rgb(0 40 53 / 90%);
}
57 changes: 57 additions & 0 deletions doc/7/getting-started/react/standalone/snippets/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from "react";

import Message from "./Message";
import { useChat } from "./useChat";
import "./App.css";

export default function App() {
const [username, setUsername] = useState(null);
const [draft, setDraft] = useState("");
const { messages, ready, sendMessage } = useChat(username);

// Ask for a nickname before joining the chat
if (!username) {
return (
<form
className="wrapper"
onSubmit={(event) => {
event.preventDefault();
setUsername(event.target.elements.username.value.trim() || null);
}}
>
<input autoFocus name="username" placeholder="Enter your nickname" />
<button type="submit">Join</button>
</form>
);
}

return (
<div>
<form
className="wrapper"
onSubmit={async (event) => {
event.preventDefault();
await sendMessage(draft.trim());
setDraft("");
}}
>
<input
autoFocus
disabled={!ready}
onChange={(event) => setDraft(event.target.value)}
placeholder="Enter your message"
value={draft}
/>
<button disabled={!ready} type="submit">
Send
</button>
</form>

<div>
{messages.map((message) => (
<Message key={message._id} message={message} username={username} />
))}
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions doc/7/getting-started/react/standalone/snippets/Message.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function Message({ message, username }) {
const origin = message.username === username ? "fromMe" : "fromOthers";

return (
<div className={`${origin} messages`}>
<span>
User: <b>{message.username}</b>
</span>
<span> ({new Date(message.createdAt).toLocaleString()})</span>
<p>{message.value}</p>
</div>
);
}
10 changes: 10 additions & 0 deletions doc/7/getting-started/react/standalone/snippets/kuzzle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Kuzzle, WebSocket } from "kuzzle-sdk";

// Replace 'localhost' with the hostname of your Kuzzle server
const kuzzle = new Kuzzle(new WebSocket("localhost"));

kuzzle.on("networkError", (error) => {
console.error("Network Error:", error);
});

export default kuzzle;
96 changes: 96 additions & 0 deletions doc/7/getting-started/react/standalone/snippets/useChat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useCallback, useEffect, useRef, useState } from "react";

import kuzzle from "./services/kuzzle";

// Turns a Kuzzle document into the shape our components expect
const toMessage = (document) => ({
_id: document._id,
value: document._source.value,
username: document._source.username,
createdAt: document._source._kuzzle_info.createdAt,
});

export function useChat(username) {
const [messages, setMessages] = useState([]);
const [ready, setReady] = useState(false);
const roomId = useRef(null);

useEffect(() => {
// Nothing to do until the user has picked a nickname
if (!username) {
return;
}

let cancelled = false;

const start = async () => {
await kuzzle.connect();

// Creates the index and the collection on first run
if (!(await kuzzle.index.exists("chat"))) {
await kuzzle.index.create("chat");
await kuzzle.collection.create("chat", "messages");
}

// Receives a notification for every new message
roomId.current = await kuzzle.realtime.subscribe(
"chat",
"messages",
{},
(notification) => {
if (notification.type !== "document") {
return;
}

if (notification.action !== "create") {
return;
}

setMessages((previous) => [
toMessage(notification.result),
...previous,
]);
},
);

// Loads the hundred most recent messages
const results = await kuzzle.document.search(
"chat",
"messages",
{ sort: { "_kuzzle_info.createdAt": "desc" } },
{ size: 100 },
);

if (cancelled) {
return;
}

setMessages(results.hits.map(toMessage));
setReady(true);
};

start().catch((error) => console.error(error.message));

return () => {
cancelled = true;

if (roomId.current) {
kuzzle.realtime.unsubscribe(roomId.current);
roomId.current = null;
}
};
}, [username]);

const sendMessage = useCallback(
async (value) => {
if (!value) {
return;
}

await kuzzle.document.create("chat", "messages", { value, username });
},
[username],
);

return { messages, ready, sendMessage };
}
Loading
Loading