Getting Started with Kuzzle and React with Redux #
This tutorial explains how to use Kuzzle with the Javascript SDK 7, React and Redux (through Redux Toolkit).
It builds the same realtime chat as the standalone React tutorial, but the messages live in a Redux store instead of a component state. Messages are stored as documents in Kuzzle, and every client is kept up to date through document notifications.
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.
Having trouble? Get in touch with us on Discord!
Requirements #
- Node.js >= 20 (download page)
- a running Kuzzle V2 stack (instructions here)
Prepare your environment #
Create a React application with Vite and install the Javascript SDK along with Redux:
npm create vite@latest kuzzle-playground -- --template react
cd kuzzle-playground
npm install
npm install kuzzle-sdk@7 @reduxjs/toolkit react-reduxThis tutorial uses Redux Toolkit, which is the approach recommended by the Redux team. The hand-written action types, switch reducers and redux-saga middleware of the older tutorials are no longer needed.
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:
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;Replace localhost with the hostname of the machine running your Kuzzle server.
Creating the store #
The store holds the message list. We need a slice with:
- a
messageReceivedreducer, fed by the realtime subscription, - a
fetchMessagesthunk that searches for the existing messages, - a
sendMessagethunk that creates a document.
Create a src/state/messagesSlice.js file:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import kuzzle from "../services/kuzzle";
// Turns a Kuzzle document into the shape our components expect
export const toMessage = (document) => ({
_id: document._id,
value: document._source.value,
username: document._source.username,
createdAt: document._source._kuzzle_info.createdAt,
});
export const fetchMessages = createAsyncThunk("messages/fetch", async () => {
const results = await kuzzle.document.search(
"chat",
"messages",
{ sort: { "_kuzzle_info.createdAt": "desc" } },
{ size: 100 },
);
return results.hits.map(toMessage);
});
export const sendMessage = createAsyncThunk(
"messages/send",
async ({ value, username }) => {
await kuzzle.document.create("chat", "messages", { value, username });
},
);
const messagesSlice = createSlice({
name: "messages",
initialState: {
list: [],
ready: false,
},
reducers: {
// Dispatched by the realtime subscription, for our own messages as well
messageReceived(state, action) {
state.list.unshift(action.payload);
},
},
extraReducers: (builder) => {
builder.addCase(fetchMessages.fulfilled, (state, action) => {
state.list = action.payload;
state.ready = true;
});
},
});
export const { messageReceived } = messagesSlice.actions;
export default messagesSlice.reducer;Note that sendMessage does not add anything to the store: the new message comes back through the realtime notification, exactly like the ones sent by the other clients. There is a single code path for every message, wherever it comes from.
Then declare the store itself in src/state/store.js:
import { configureStore } from "@reduxjs/toolkit";
import messagesReducer from "./messagesSlice";
export default configureStore({
reducer: {
messages: messagesReducer,
},
});And make it available to the whole application by wrapping it in a Provider, in src/main.jsx:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import store from "./state/store";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);Connecting to Kuzzle #
All the Kuzzle lifecycle lives in a single hook, which dispatches into the store. Create a src/useKuzzleSync.js file:
import { useEffect, useRef } from "react";
import { useDispatch } from "react-redux";
import kuzzle from "./services/kuzzle";
import {
fetchMessages,
messageReceived,
toMessage,
} from "./state/messagesSlice";
export function useKuzzleSync(username) {
const dispatch = useDispatch();
const roomId = useRef(null);
useEffect(() => {
// Nothing to do until the user has picked a nickname
if (!username) {
return;
}
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");
}
// Every new message is pushed into the store by the reducer
roomId.current = await kuzzle.realtime.subscribe(
"chat",
"messages",
{},
(notification) => {
if (notification.type !== "document") {
return;
}
if (notification.action !== "create") {
return;
}
dispatch(messageReceived(toMessage(notification.result)));
},
);
dispatch(fetchMessages());
};
start().catch((error) => console.error(error.message));
return () => {
if (roomId.current) {
kuzzle.realtime.unsubscribe(roomId.current);
roomId.current = null;
}
};
}, [dispatch, username]);
}Once the user has chosen a nickname, this hook:
- connects the SDK to Kuzzle,
- creates the
chatindex and themessagescollection if they don't exist yet, - subscribes to the collection and dispatches
messageReceivedon every document creation, - dispatches
fetchMessagesto fill the history.
The subscription is opened before the history is fetched, and the cleanup function unsubscribes 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.
Displaying the messages #
Create a src/Message.jsx component to render a single message:
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>
);
}Then add the styles in src/App.css:
.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%);
}Putting it together #
Finally, rewrite src/App.jsx. It reads the messages from the store with useSelector, and sends new ones by dispatching the sendMessage thunk:
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import Message from "./Message";
import { useKuzzleSync } from "./useKuzzleSync";
import { sendMessage } from "./state/messagesSlice";
import "./App.css";
export default function App() {
const [username, setUsername] = useState(null);
const [draft, setDraft] = useState("");
const dispatch = useDispatch();
const { list: messages, ready } = useSelector((state) => state.messages);
useKuzzleSync(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={(event) => {
event.preventDefault();
const value = draft.trim();
if (value) {
dispatch(sendMessage({ value, username }));
}
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>
);
}Launch the application:
npm run devOpen 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. Install the Redux DevTools extension to watch the messages/messageReceived actions as they arrive.
Going further #
Now that you are more familiar with Kuzzle, dive even deeper to learn how to leverage its full capabilities:
- Discover what this SDK has to offer by browsing other sections of this documentation
- Learn more about Kuzzle realtime engine
- Learn how to use the Kuzzle Admin Console to manage your users and data
- Learn how to use Koncorde 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