Skip to Content
Technical Articles

UI5 Web Components + React + Redux + Socket.io

Hi all!!

It has been around 8 months since I wrote my last line of UI5 code, and you know… After 2 years being deeply involved with it… I kinda miss it.

In the past, this same feeling drove me to solve many questions in stackoverflow, but I don’t enjoy it that much now. I needed something new… Aaaaand last Friday reviewing my feeds here and there, I found this article.

I can give you a tip here ( just in the first paragraph hahaha ): Do not skip a post from Peter Muessig  it always means that there is something interesting around.

Anyway, coding stuff. Due to my last 8 months playing with Angular, React, Redux, blah blah blah, I saw in that article the light!! Let’s have some UI5-fun again.

The idea:

Mix these UI5 web components with a bunch of things like React, Redux and Socket.io for example. I want to see how good they work together, so it won’t be a tutorial about how to master UI5 Web Components. Follow the SAP tutorials for that.

The disclaimer:

I am coding it while writing this article (I will forget some details if I write it afterwards). Do not expect clean code or best practices… This is just mind vomit.

The prerequisites:

I know a bit of ReactJS, just the basics of Redux and I did a couple of “I am bored, let’s do something” projects with Socket.io. To follow this post, I guess you should be at least in the same level. But keep reading… maybe I am wrong, or you think you know less than what you really know.

 

STEP 1: Clone the React-UI5 app already available

Peter gave me the following link when answering my comment on the already mentioned article:

https://sap.github.io/ui5-webcomponents/

Just scroll down a bit and hit the ‘Explore the code’ link for the React App (but before you leave, save the link for later, it can give you more Angular/Vue/whatever fun later)

Follow the first steps described on the git repo. For me:

  1. git clone https://github.com/SAP/ui5-webcomponents-sample-react.git
  2. cd ui5-webcomponents-sample-react
  3. npm install
  4. npm start

Ahhaaa!! ToDo list!! What a classic… I love it.

NOTE: If you’ve reached this point, I hope you have the same feeling I have right now => “I already hosted an app on my localhost… Jesus! I’m a PRO !!”

 

STEP2: Explore the app + save my carrots

Go to the always-magic folder: ‘src’.

The first file I tackle is `App.js`.

Parenthesis: (mental question for the folks in the UI5 team: Why no .jsx extension? I know there is an endless discussion about it, but it would be great to know why the mighty SAP chose .js)

In the `App.js` file, after the imports, there is a long state definition.

state = {
    todos: [
      {
        text: "Get some carrots",
        id: 1,
        deadline: "27/7/2018",
        done: false
      },
      {
        text: "Do some magic",
        id: 2,
        deadline: "22/7/2018",
        done: false
      },
      {
        text: "Go to the gym",
        id: 3,
        deadline: "24/7/2018",
        done: true
      },
      {
        text: "Buy milk",
        id: 4,
        deadline: "30/7/2018",
        done: false
      },
      {
        text: "Eat some fruits",
        id: 5,
        deadline: "29/7/2018",
        done: false
      }
    ],
    todoBeingEdittedText: "",
    todoBeingEdittedDate: "",
    selectedEditTodo: ""
};

It seems like the node ‘todos’ is the initial data, the one showed in the todo list app.

I don’t like it in the state, basically because it is volatile, I want to persist this data and whatever change I make to it via app. SAVE MY CARROTS!!

For those who don’t know me, I am one of these “full stackers” that understand how to do Frontend and suffer during hours to connect a real DB. So sorry, no mongo DB this time. The idea is to use json-server (https://github.com/typicode/json-server) and serve a fake RESTfull API. Very easy if you follow the steps in the json-server repo.

For me:

  1. Open a new terminal and cd into the app folder
  2. Run: npm install -g json-server
  3. Create a db.json file somewhere (for me in a `data` folder into the root)
  4. Copy + Paste the `todos` object into the db.json file
  5. Run: json-server –watch -p 3001 data/db.json

I like to run the BE server in a different port than the FE server, that’s why 3001… but do it as you like.

So far so good, I still have my demo app in `localhost:3000` and now I can do `localhost:3001/todos` and get my list of todos from my fake BE server (The slim brother of the SAP NW Gateway 7.5)

 

STEP 3: Fetch those `todos` from our fake DB

If you are curious enough you already realised that deleting the `todos` node from the state will break the app. But if you empty the array, the app runs and shows an empty list.

So let’s fetch them before we delete them. Right now I’ll do it in the `App.js` file, in the componentWillMount() method. We might change it later.

componentWillMount(){
  fetch("http://localhost:3001/todos")
  .then(res => res.json())
  .then(todos => console.log(todos));
}

All good, If you check the browser console in the UI5 App, you should see the fetched `todos` there.

Let’s put them in the state. First empty the `todos` array in the state definition after the imports (if you save it, your app should show an empty list) and then change:

console.log(todos)

For:

this.setState({todos})

And that’s it. Your items come from your fake DB. If you want to double check it, change the db.json file as you like and see how the app updates as well.

 

Step 4: Redux – Chapter 1: The R in CRUD

All right, this is awkward. I want to add a bit of detail in this post, but if I try to explain what is Redux in detail and how to set the boilerplate step by step, this post will take forever. So I have decided to code it, mention a few important things and give you the code. I am sure you will manage to understand it.

WHAT’S REDUX??

“Redux is a predictable state container for JavaScript apps.” – quoted from https://redux.js.org/introduction/getting-started

I don’t know/care what that means, but I know that it allows you to manage a global state in your app and somehow “subscribe” your components to this global state, so they are re-rendered if the bound data change.

WHY REDUX??

It is kinda mandatory thing nowadays for ReactJS big projects that move a lot of data. To be honest, I found it very handy for many things.

Redux works great with ReactJS (like Timon and Pumbaa), BUT the required boilerplate config is awful. You can find many tutorials out there, I cannot explain it here. If you don’t want to spend too much time on it:

1. copy the ‘src/redux’ folder from my repo into your app.

2. Run: npm install –save redux react-redux redux-thunk

3. Create a file called Main.js in the `src` folder with the following content

import React, { Component } from 'react';
//Components
import App from './App'
//Redux
import { Provider } from 'react-redux'
import store from './redux/store'

class Main extends Component {
  render() {
    return (
      <Provider store={store}>
        <div className="app">
          <App/>
        </div>
      </Provider>
    );
  }
}

export default Main;

4. Change the index,js file to use the new Main.js component as ‘root’ component

import Main from './Main';
...
ReactDOM.render(<Main />, document.getElementById('root'));

5. Import connect and fetchTodos in the App.js file, create the mapStateToProps() function and return the component wrapped into the connect function

import { connect } from 'react-redux'
import { fetchTodos } from './redux/actions/todoActions'
…
const mapStateToProps = (state) => ({
  todos: state.todos.items
});

export default connect(mapStateToProps, { fetchTodos })(App)

NOTE: If you have any issue here, try to restart the FE server: npm start

NOTE2: Sorry for the .jsx extension on my ‘redux’ files. I copied the boilerplate from another test app I did before and I am a bit lazy to change the files extension.

All right, nice milestone here. We have the Redux config ready. And if you have installed the super handy Redux Chrome Extension you can see the @@init action in the history. Let’s use Redux then.

Fetch

First we want to call the fetchTodos Action creator -> it will fetch our todos from our fake DB and add them to the global state. So we don’t need the componentWillMount() method we created in STEP 3 anymore. Delete it completely.

Now go to the componentDidMount() method and add one line:

this.props.fetchTodos();

This (together with all the previous config) will fire the GET request, set the global state, and map our todos from the global state with the props in the App component. MAGIC!!

If you check the Redux Chrome Extension again, you will see a new action ‘FETCH_TODOS’ in the history

As you can imagine we just need to bind the UI5 Web Components with the todos in this.props.todos instead of the todos in this.state.todos.

To make it faster, search in your code for `this.state.todos` and change it only in the following 3 places:

<div className="list-todos-wrapper">
   <TodoList
      items={ this.props.todos.filter(todo => !todo.done)}
      selectionChange={this.handleDone.bind(this)}
      remove={this.handleRemove.bind(this)}
      edit={this.handleEdit.bind(this)}
   >
   </TodoList>

   <ui5-panel header-text="Completed tasks" collapsed={!this.props.todos.filter(todo => todo.done).length || undefined}>
      <TodoList
         items={this.props.todos.filter(todo => todo.done)}
         selectionChange={this.handleUnDone.bind(this)}
         remove={this.handleRemove.bind(this)}
         edit={this.handleEdit.bind(this)}
       >
       </TodoList>
    </ui5-panel>
</div>

Now you should see the app with the data coming from the global state. Thank you Redux!!

 

Step 5: Redux – Chapter 2: The CUD in CRUD

Yes, yes… I know… I broke the app and we can’t do anything now… Let’s fix some of the features step by step.

As the worldwide-known Ivan Klima said: ‘To destroy is easier than to create’. Roger that!! Let’s Delete todos.

In the App.js file:

1. Import deleteTodos action creator:

import { fetchTodos, deleteTodo } from './redux/actions/todoActions'

2. Connect it at the end of the file:

export default connect(mapStateToProps, { fetchTodos, deleteTodo })(App)

Now we can use the delete action. And we will do it in the handleRemove() event handler. Feel free to remove all what’s in that method and add this line:

this.props.deleteTodo(id)

And that’s it! We recovered the Delete functionality and saved several lines of code. As beautiful and clean as abstract… This is Redux…

Ok, let’s do it faster for Create and Update.

CREATE:

1. Import createTodo action creator and connect it:

import { fetchTodos, deleteTodo, createTodo } from './redux/actions/todoActions'
…
export default connect(mapStateToProps, { fetchTodos, deleteTodo, createTodo })(App)

2. Edit the handleAdd() event handler as follows:

handleAdd() {
    let newTodo = {
      text: this.todoInput.current.value,
      id: this.props.todos.length + 1,
      deadline: this.todoDeadline.current.value,
      done: false
    }
    this.props.createTodo(newTodo)
}
UPDATE:

For this one I have decided to do the Done/UnDone update, and leave the form for you… After all, I think there is enough information and code here to be able to do the full ‘Edit Todo’ by yourself.

1. Import editTodo action creator and connect it:

import { fetchTodos, deleteTodo, createTodo, editTodo } from './redux/actions/todoActions'
...
export default connect(mapStateToProps, { fetchTodos, deleteTodo, createTodo, editTodo })(App)

2. Edit the handleDone() and handleUnDone() event handlers as follows:

handleDone(event) {
  const selectedItem = event.detail.items[0];
  const selectedId = selectedItem.getAttribute("data-key");
  let newData = {
    done: true
  }
  this.props.editTodo(selectedId, newData);
}

handleUnDone(event) {
  const itemsBeforeUnselect = event.currentTarget.items;
  const itemsAfterUnselect = event.detail.items;
  const changedItem = itemsBeforeUnselect.filter(item => !itemsAfterUnselect.includes(item))[0]
  let newData = {
    done: false
  }
  this.props.editTodo(changedItem.getAttribute("data-key"), newData);
}

 

Step 6: WebSockets – The RealTime Magic

All right! We are almost at the end… and I want to do something a bit weird. Take this step as an extra step. You don’t have to do it, but as some people taught me in the USA:

“If you can do it, overdo it”.

The idea is to set a web socket that notifies the app when the list of todos has changed, sending the new data.

Let’s go:

  1. Install Socket.io: npm install –save socket.io
  2. Create a folder called ‘socket-api’ in the ‘src’ folder
  3. Create a file called server.js in the ‘src/socket-api’ folder
  4. Add the following code:
/**
 * Socket.io
 */
const io = require('socket.io')();

// Config
const port = 8000;
io.listen(port);
console.log('listening on port ', port);

// Events
io.on('connection', (client) => {
  console.log("New connection");
});

5a.  Open a new terminal, cd into the ‘src/socket-api’ folder and run: node server.js

5b.  I recommend you to install nodemon (npm install -g nodemon) and run ‘nodemon server.js‘ instead of ‘node server.js‘. With nodemon you don’t have to restart the server after modifying the server.js file. Nodemon will do it for you automatically.

Voila!! We have a dummy server on http://localhost:8000 listening for connections.

Now the client side:

  1. Install socket.io-client: npm install –save socket.io-client
  2. Create a file called ‘socket-api.js’ in the src/socket-api folder.
  3. Add the following code:
import openSocket from 'socket.io-client';
var socket = openSocket('http://localhost:8000');

export default socket;

export function socket_init(){
    console.log('connected to socket')
}

Now go to your index.js file in the ‘src’ folder and add the following lines:

import { socket_init } from './socket-api/socket-api';
. . .
socket_init();

And done!! If everything was good you should have a ‘New connection’ message in your terminal where you are running the server.js file and a ‘connected to socket’ message in your browser console.

Now we have to emit/subscribe to events. I want to:

  • Emit events from the client to the server when I CRUD todos.
  • The server fires the request to the DB and, once finished, emit an event to the client with the whole list of todos.

NOTE: We will do some extra DB calls, which probably is not the optimal solution, but anyway… let’s do it.

How to make it work with Redux?

Well, I will define a new Action of type NEW_TODOS_DATA. This action subscribes to the ‘newTodosData’ socket event. Whenever the client listen this event in the socket, it dispatches NEW_TODOS_DATA action with the new data. The reducer for NEW_TODOS_DATA is the only one that sends the new todos to the store, and therefore is the only one that creates a new global state. All the other action creators will only emit events through the socket towards the server. Then the server processes the received event, and once it finishes, it emits the ‘newTodosData’ web socket event again to the client, and the NEW_TODOS_DATA action starts again.

As I said before, I don’t want to go too deep into the Redux Actions and Reducers, so you can copy the ‘redux’ folder again. This time from this repo

READ:

1. First, install Axios to do http request easily. Run: npm install –save axios

2. In the `socket-api/server.js` file, import axios and listen for an event called ‘getTodos’. When a client emit that event, make a DB request with axios and emit another event called ‘newTodosData’ towards the client:

const axios = require('axios');
const DB_HOST = 'http://localhost:3001';
...
// Events
io.on('connection', (client) => {
  client.on('getTodos', () => {
    axios.get(DB_HOST+'/todos')
    .then(response => {
      client.emit('newTodosData', response.data);
    })
    .catch(error => {
      console.log(error);
    });
  });
});

3. If you copy the files from my ‘redux’ folder from the repo I just mentioned, you can see that the main changes are in the action creators (take a look at the code comments):

/**
 * Subscribes to newTodosData socket event.
 * On every occurrence, dispatches the new data to the corresponding reducer 
 */
export function subscribeNewTodos(){
    return function(dispatch){
        socket.on('newTodosData', newTodos => {
            dispatch({
                type: NEW_TODOS_DATA,
                payload: newTodos
            });
        })
    }
}

/**
 * Emits a getTodos socket event
 */
export function fetchTodos(){
    socket.emit('getTodos');
    return function(dispatch){
        dispatch({
            type: FETCH_TODOS
        });
    }
}

And the reducers (as you can see only the NEW_TODOS_DATA reducer creates a different state):

export default function (state = initialState, action) {
    switch (action.type) {
        case FETCH_TODOS:
            return {
                ...state
            };

        ...

        case NEW_TODOS_DATA:
            return {
                ...state,
                items: action.payload
            };

        ...

    }
}​

4. Last but not least in the App.js file, we have to import the new ‘subscribeNewTodos’ action, connect it and call it:

import { fetchTodos, deleteTodo, createTodo, editTodo, subscribeNewTodos } from './redux/actions/todoActions'
...
export default connect(mapStateToProps, { fetchTodos, deleteTodo, createTodo, editTodo, subscribeNewTodos })(App)

And in the componentDidMount() function add:

this.props.subscribeNewTodos();

before:

this.props.fetchTodos();

 

CREATE, UPDATE, DELETE:

Ok there aren’t many new fancy things now. All we have to do is change our Redux action creators to emit the create/update/delete socket event towards the server and define the event handlers in the server side to send the request to the DB. Afterwards, emit the ‘newTodosData’ with the new data towards the client.

1. Define these new event listeners in the server.js file:

...

// Events
io.on('connection', (client) => {

    ...

    //Delete one Todo
    client.on('deleteTodo', (data) => {
        axios.delete(DB_HOST+'/todos/'+data.id)
        .then(response => {
            //Get all todos again
            axios.get(DB_HOST+'/todos')
            .then(response => {
                client.emit('newTodosData', response.data);
            })
            .catch(error => {
                console.log(error);
            });
        })
        .catch(error => {
            console.log(error);
        });
    });
    //Create one Todo
    client.on('createTodo', (data) => {
        axios.post(DB_HOST+'/todos/', data.todo)
        .then(response => {
            //Get all todos again
            axios.get(DB_HOST+'/todos')
            .then(response => {
                client.emit('newTodosData', response.data);
            })
            .catch(error => {
                console.log(error);
            });
        })
        .catch(error => {
            console.log(error);
        });
    });
    //Create one Todo
    client.on('editTodo', (data) => {
        axios.patch(DB_HOST+'/todos/'+data.id, data.newData)
        .then(response => {
            //Get all todos again
            axios.get(DB_HOST+'/todos')
            .then(response => {
                client.emit('newTodosData', response.data);
            })
            .catch(error => {
                console.log(error);
            });
        })
        .catch(error => {
            console.log(error);
        });
    });
});

2. Now that the server listens to us, let’s set the event emitters. In the ‘redux/action/todoActions.jsx’ modify the following action creators

/**
 * Emits a createTodo socket event
 */
export function createTodo(todo){
    socket.emit('createTodo', { todo });
    return function(dispatch){
        dispatch({
            type: NEW_TODO
        });
    }
}

/**
 * Emits a deleteTodo socket event
 */
export function deleteTodo(id){
    socket.emit('deleteTodo', { id });
    return function(dispatch){
        dispatch({
            type: DELETE_TODO
        });
    }
}

/**
 * Emits a editTodo socket event
 */
export function editTodo(id, newData){
    socket.emit('editTodo', { id, newData });
    return function(dispatch){
        dispatch({
            type: EDIT_TODO
        });
    }
}

As you can see the action creators just emit the socket event with the corresponding data, and then dispatch the action with no data.

3. Now adjust the reducers accordingly:

export default function (state = initialState, action) {
    switch (action.type) {
        case FETCH_TODOS:
            return {
                ...state
            };
        case NEW_TODO:
            return {
                ...state
            };
        case DELETE_TODO:
            return {
                ...state
            };
        case EDIT_TODO:
            return {
                ...state
            }
        case NEW_TODOS_DATA:
            return {
                ...state,
                items: action.payload
            };
        default:
            return state;
    }
}

And that’s it!! Our UI5 app is running with React+Redux and talking to a server via WebSockets.

 

Conclusion

I love exploring these possibilities, and I hope you like it as well. I think SAPUI5 folks have made a very good decision stepping into WebComponents. It gives you enormous flexibility with all the products/tools out there and it is super fast to code and deploy.

I can imagine how easy it would be now to build some UI5+Redux apps connecting with your API endpoints. Refactoring your non-UI5 apps into UI5 flavored apps. Deploy them into an AWS S3 bucket and link a tile in your Fiori Launchpad to that S3 bucket. No extra files, no CDN, etc etc… Really cool.

That’s it! I hope this post helps you to open your mind for new ideas…

LAST NOTE: I don’t check for updates in these posts regularly, so if you have any questions or crazy ideas and you want to contact me, feel free to ping me on LinkedIn: https://www.linkedin.com/in/rafaellopezmartinez/

Cheers,

Rafa

 

 

 

 

 

 

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.