Getting Started with Kuzzle and React with Redux Saga #
This section deals with Kuzzle (+ Javascript SDK) and React (with Redux and Redux Saga). We will create documents in Kuzzle and subscribe to document notifications to develop a realtime chat.
Requirements #
- Node.js >= 8.0.0 (install here)
- Create React App (install here)
- Running Kuzzle Stack (instructions here)
Prepare your environment #
Create your React app and install all the dependencies from the command line using yarn:
yarn create react-app kuzzle-playground
cd kuzzle-playground
yarn add kuzzle-sdk redux redux-saga react-redux
We'll rewrite the src/App.js so you can remove everything inside.
Instantiating Kuzzle SDK #
We have to connect the server so that our client can interact with it.
To do this, we have to create src/services/kuzzle.js file to put our kuzzle instance, a bit like a singleton:
import { Kuzzle, WebSocket } from 'kuzzle-sdk';
export default new Kuzzle(new WebSocket('localhost'));
You can now edit the src/App.js file to connect to Kuzzle. To do this, import the kuzzle service file:
import kuzzle from './services/kuzzle';
Add the following imports in the same time:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
import ActionCreators from './state/actions';
Now, add the app
class and add in the constructor a message
property in the state (we'll use it to store the user input message) and a call to an _initialize()
function:
class App extends Component {
constructor(props) {
super(props);
this.state = {
message: ''
};
this._initialize();
}
After that, create that function with the connection to Kuzzle:
async _initialize() {
// handler to be notified in case of a connection error
kuzzle.on('networkError', error => {
console.error(error.message);
});
await kuzzle.connect();
Then we will establish the connection to kuzzle and create, if they don't exist, the index and collection for our chat.
Add the following lines to the _initialize
function:
const exists = await kuzzle.index.exists('chat');
if (!exists) {
await kuzzle.index.create('chat');
await kuzzle.collection.create('chat', 'messages');
}
Display the messages #
We'll need some properties and functions to manage our messages.
We have to create our Redux store architecture (more details on Redux documentation), like this:
src
└── state
├── actions.js
├── reducers.js
└── sagas.js
Add the following actions to the src/state/actions.js file:
const ActionCreators = {
sendMessage: text => ({
type: 'SEND_MESSAGE',
payload: {
text
}
}),
setMessages: messages => ({
type: 'SET_MESSAGES',
payload: {
messages
}
})
};
export default ActionCreators;
Then we'll edit the src/state/reducers.js file:
Add the initialState
:
const initialState = {
messages: []
};
And the reducersMap
with our SET_MESSAGE
action:
const reducersMap = {
SET_MESSAGES: (state, payload) => {
console.log(state, payload)
return {
messages: [...state.messages, ...payload.messages]
}
},
leaveStateUnchanged: state => state
};
Finally, export it:
export default function reducers(state = initialState, action) {
const reducer = reducersMap[action.type] || reducersMap.leaveStateUnchanged;
const newState = reducer(state, action.payload, action.meta);
return newState;
}
The entire file should look like this:
const initialState = {
messages: []
};
const reducersMap = {
SET_MESSAGES: (state, payload) => {
console.log(state, payload)
return {
messages: [...state.messages, ...payload.messages]
}
},
leaveStateUnchanged: state => state
};
export default function reducers(state = initialState, action) {
const reducer = reducersMap[action.type] || reducersMap.leaveStateUnchanged;
const newState = reducer(state, action.payload, action.meta);
return newState;
}
Now that our store is ready, we'll fetch the existing messages in Kuzzle and add them to our store. Add the following lines to the _initialize()
function of the app
class in the src/App.js file:
const results = await kuzzle.document.search(
'chat',
'messages',
{} // leave body empty to match all documents
);
if (results.total > 0) {
this.props.setMessages(results.hits.map(hit => hit._source));
}
this._subscribeToNewMessages();
Then, add the following constants in the render()
function of the app
class:
render() {
const { messages } = this.props;
const { message } = this.state;
And the loop in the return of the render()
function to display the messages stored:
<div>
{[...messages].reverse().map(message => (
<p key={messages.indexOf(message)}>{message.text}</p>
))}
</div>
We can now display the messages stored in Kuzzle. In the next part, we'll see how to create new messages.
Send messages #
We need to write a simple method that will create a new message document in Kuzzle. Add the following function in your app
class in the_src/App.js_ file:
sendMessage = event => {
this.props.sendMessage(this.state.message);
this.setState({
message: ''
});
};
Then, we need to create the sendMessage()
Redux action we just called. src/state/sagas.js contains a generator function where we will put our sagas function. (more details on Redux-saga documentation):
Let's add it in the src/state/sagas.js file:
import { takeEvery } from 'redux-saga/effects';
import kuzzle from '../services/kuzzle';
const sendMessage = function*({ payload: { text } }) {
try {
const document = {
text
};
yield kuzzle.document.create('chat', 'messages', document);
} catch (e) {
console.error(e);
}
};
export default function*() {
yield takeEvery('SEND_MESSAGE', sendMessage);
}
As you can see we don't push the new message in our state on message creation. Now, we need to subscribe to changes made on the collection containing our messages. So let's create our _subscribeToNewMessages()
function in the app
class in src/App.js file. It will call Kuzzle's realtime controller to allow us to receive notifications on message creations:
async _subscribeToNewMessages() {
kuzzle.realtime.subscribe('chat', 'messages', {}, notif => {
if (!(notif.type === 'document' && notif.action === 'create')) {
return;
}
const { _source: message } = notif.result;
this.props.setMessages([message]);
});
}
Then, just add an input field bound to the message
property, and a button calling our sendMessage()
function:
<div>
<input
type="text"
name="message"
id="message"
value={message}
onChange={this.handleChange}
/>
<button onClick={this.sendMessage}>Envoyer</button>
</div>
We need to update our message
state property when this input changes. To do that, the onChange
event is bound to an handleChange()
method. Let's create in the app
class:
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
};
To finish, just add the export to the src/App.js file:
// connect to redux store
export default connect(
state => ({
messages: state.messages
}),
{
sendMessage: ActionCreators.sendMessage,
setMessages: ActionCreators.setMessages
}
)(App);
The entire file should look like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
import ActionCreators from './state/actions';
import kuzzle from './services/kuzzle';
class App extends Component {
constructor(props) {
super(props);
this.state = {
message: ''
};
this._initialize();
}
async _initialize() {
// handler to be notified in case of a connection error
kuzzle.on('networkError', error => {
console.error(error.message);
});
await kuzzle.connect();
const exists = await kuzzle.index.exists('chat');
if (!exists) {
await kuzzle.index.create('chat');
await kuzzle.collection.create('chat', 'messages');
}
const results = await kuzzle.document.search(
'chat',
'messages',
{} // leave body empty to match all documents
);
if (results.total > 0) {
this.props.setMessages(results.hits.map(hit => hit._source));
}
this._subscribeToNewMessages();
}
async _subscribeToNewMessages() {
kuzzle.realtime.subscribe('chat', 'messages', {}, notif => {
if (!(notif.type === 'document' && notif.action === 'create')) {
return;
}
const { _source: message } = notif.result;
this.props.setMessages([message]);
});
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
};
sendMessage = event => {
this.props.sendMessage(this.state.message);
this.setState({
message: ''
});
};
render() {
const { messages } = this.props;
const { message } = this.state;
return (
<div>
<div>
<input
type="text"
name="message"
id="message"
value={message}
onChange={this.handleChange}
/>
<button onClick={this.sendMessage}>Envoyer</button>
</div>
<div>
{[...messages].reverse().map(message => (
<p key={messages.indexOf(message)}>{message.text}</p>
))}
</div>
</div>
);
}
}
// connect to redux store
export default connect(
state => ({
messages: state.messages
}),
{
sendMessage: ActionCreators.sendMessage,
setMessages: ActionCreators.setMessages
}
)(App);
To launch this app, just type the following command:
yarn start
You can now add new messages to Kuzzle and receive the notification of the creation to update your state and display the new messages.
Going further #
Now that you're more familiar with Kuzzle with React, you can:
- discover what this SDK has to offer by browsing other sections of this documentation
- learn how to use Koncorde to create incredibly fine-grained and blazing-fast subscriptions
- learn more about Kuzzle realtime engine
- follow our guide to learn how to manage users, and how to set up fine-grained access control
To help you starting a new project with Kuzzle and React, you can start with the Kuzzle, React and Redux boilerplate.