UI5 Logo

UI5 Web Components + React + Redux + Socket.io

A few months ago I read a post about the release of UI5 Web Components by Peter Müssig, one of the fathers of SAP UI5 I worked with during my period working for the german company. 

At that time I was living my 8th month after leaving SAP and working for the Movember Foundation in Melbourne. As you can imagine, my contact with UI5 became minimal since I left SAP. Some answers on Stackoverflow, some job interviews and not much more… But I reckon I kinda miss it after being 2 years digging into the framework every single day.

That’s why when I read that post about UI5 Web Components I had no doubt. I had to play with it and mix it with the technologies I was working with while working at Movember. React, Redux and Socket.io. I stepped into it and I wrote this post in the SAP Comunity, which I copy below.

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

The UI5 team released some example apps using several technologies. You can find them clicking here.

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:

git clone https://github.com/SAP/ui5-webcomponents-sample-react.git
cd ui5-webcomponents-sample-react
npm install
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

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 and serve a fake RESTfull API. Very easy if you follow the steps in the json-server repo.

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 in the project (For me, the folder «/data«)

4. Copy and paste the todos object in the db.json file

5. Run: json-server –watch -p 3001 data/db.json

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:

Delete:

In the file App.js:

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»  – a friend from USA

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 the folder «src/socket-api«

3. Create a file called server.js in that 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:

1. Emit events from the client to the server when I CRUD todos.

2. 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.

And this is it! I hope this post helps you to open your mind and explore new ideas… Do not hesitate to leave a comment below to let me know what you think. And if you have any idea that you want me to analyze in another post, feel free to write it as well. I’ll try to do it ASAP.

Cheers!

Rafa

Comparte este post

Share on whatsapp
Share on telegram
Share on facebook
Share on twitter
Share on linkedin
Share on email