Getting Started with Kuzzle and React with Redux Saga

This section deals with Kuzzle V2 (+ Javascript SDK 7) and React (with Redux and Redux Saga). We will create documents in Kuzzle and subscribe to document notifications to develop a realtime chat.

Requirements

Prepare your environment

Create your React app and install all the dependencies from the command line using yarn:

Copied to clipboard!
yarn create react-app kuzzle-playground
cd kuzzle-playground
yarn add kuzzle-sdk@7 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:

Copied to clipboard!
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:

Copied to clipboard!
import kuzzle from './services/kuzzle';

Add the following imports in the same time:

Copied to clipboard!
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:

Copied to clipboard!
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: ''
    };
    this._initialize();
  }

After that, create that function with the connection to Kuzzle:

Copied to clipboard!
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:

Copied to clipboard!
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:

Copied to clipboard!
src
└── state
    ├── actions.js
    ├── reducers.js
    └── sagas.js

Add the following actions to the src/state/actions.js file:

Copied to clipboard!
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:

Copied to clipboard!
const initialState = {
  messages: []
};

And the reducersMap with our SET_MESSAGE action:

Copied to clipboard!
const reducersMap = {
  SET_MESSAGES: (state, payload) =>  {
    console.log(state, payload)
   return  {
     messages: [...state.messages, ...payload.messages]
  }
},
  leaveStateUnchanged: state => state
};

Finally, export it:

Copied to clipboard!
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:

Copied to clipboard!
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:

Copied to clipboard!
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:

Copied to clipboard!
render() {
  const { messages } = this.props;
  const { message } = this.state;

And the loop in the return of the render() function to display the messages stored:

Copied to clipboard!
<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:

Copied to clipboard!
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:

Copied to clipboard!
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:

Copied to clipboard!
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:

Copied to clipboard!
<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:

Copied to clipboard!
handleChange = event => {
  this.setState({
    [event.target.id]: event.target.value
  });
  };

To finish, just add the export to the src/App.js file:

Copied to clipboard!
// 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:

Copied to clipboard!
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:

Copied to clipboard!
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: