Last update on Sunday, November 5th 2017

Coupling a Stencil TODO app with Redux

In the previous tutorial, we used Stencil to create a TODO application.
The state of the application was built upon a Parent-Children System.
A Redux Store can be implemented to make our life easier and that's our new goal today. If you are not familiar with Redux, I'd advise you to check this tutorial, otherwise you will be quite lost.

Redux Store Manager

We start by grabbing the Redux library:

npm i --save redux

Every changes will happen on the same Redux Store. The TodoList and Todo Components will trigger actions on this store.

Both of them will use the following custom StoreManager Service:

import { Component, State } from "@stencil/core";

@Component({
  tag: "store-manager"
})
export class StoreManager {
  @State() store;

  constructor() {}

  create(value) {
    this.store.dispatch({
      type: "CREATE",
      payload: { id: Date.now(), value: value }
    });
  }

  delete(id) {
    this.store.dispatch({ type: "DELETE", payload: { id: id } });
  }

  update(id, value) {
    this.store.dispatch({ type: "UPDATE", payload: { id: id, value: value } });
  }

  setStore(store) {
    this.store = store;
  }
}

As usual, we have the traditional create, delete and update methods.
The only originality here is the setStore method. We will use this one to pass the shared Redux store later.

Ok, we now have our StoreManager.

TodoList Component

We can now go to the TodoList Component:

import { createStore } from 'redux';
import { StoreManager } from '../store/store-manager';

@Component({
  tag: 'todo-list',
  styleUrl: 'todo-list.scss'
})
export class TodoList {
  @State() todos: any;
  @State() newTodo;
  @State() store;

  @Prop({ connect: 'store-manager' }) storeManagerLoader;

  @State() storeManager: StoreManager;
  ...
}

Two new imports here:

  1. StoreManager: No surprise
  2. createStore: The function that will create our Redux Store

We have one new store State that we will use later.

Now the tricky part.
The same StoreManager must be shared between components, Dependency Injection isn't (yet?) implemented in Stencil.
We can import the Class, however, if we use it has it is, it will just create a new StoreManager every time.
I found a very interesting issue on Dependency Injection in Stencil. The connect parameter allows us to link to the component that's using the "store-manager" tag.

Thanks to this, we can do that:

  getStoreService() {
    this.storeManagerLoader.componentOnReady().then((cmp) => {
      this.storeManager = cmp.$instance;
      this.storeManager.setStore(this.store);
    });
  }

We use the componentOnReady method to listen for the StoreManager Component creation.
When it's created, we ask for the instance. We are doing approximately the same in the Todo component:

  getStoreService() {
    this.storeManagerLoader.componentOnReady().then((cmp) => {
      this.storeManager = cmp.$instance;
    });
  }

We just have one more line with the setStore method because the TodoList Component will be the one that will create the Redux store.

We can now have a look at the TodoList componentWillLoad hook:

  componentWillLoad() {
    this.getStoreService();

    const preloadedState = [ {id:1, value:'Hello'} ];

    this.store = createStore<any>(this.todoReducer, preloadedState);

    this.todos = this.store.getState();

    this.store.subscribe(() =>{
      this.todos = this.store.getState();
    });
  }

The getStoreService method is used.
We create some predefined data for our Redux Store and instantiate it. The todos State will be initiated with the Redux store's initial state.
Finally, we subscribe to the store. When the Redux store will change, the todos State will be updated with the new Redux state.
Let's not skip the todoReducer. This is the same mechanism we used in the previous Stencil tutorial:

  todoReducer(state, action) {
    switch (action.type) {
      case 'CREATE':
        state = [...state, action.payload];
        return state;
      case 'DELETE':
        state = state.filter((todo) => {
          return todo.id !== action.payload.id;
        });
        return state;
      case 'UPDATE':
        const todos = state.concat([]);

        let todoToUpdate = todos.filter((todo) => {
          return todo.id === action.payload.id;
        })[0];

        todoToUpdate.value = action.payload.value;

        state = todos;
        return state;
      default:
        return state;
    }

We can now use the StoreManager:

  addNewTodo (newTodo) {
    this.storeManager.create(newTodo.value);
  }

And update our render method:

  render() {
    return(
      <div>

        <input onChange={e => this.addNewTodo(e.target)} />

        <ul>

          {this.todos
            ? <div>
              {this.todos.map((todo) => {
                return <my-todo
                         id={todo.id}
                         value={todo.value}></my-todo>
              })}
            </div>
            : <div></div>
          }
        </ul>

      </div>
    )
  }

We got some new syntax here.
Instead of using an if, the ternary expression is used.
If we are not careful enough, the render method will try to use the todos State while it's still undefined and an error will be shown in the console until the todos State is initiated. To be clean, we will show an empty <div> when the todose State is not ready.

We can now head to the Todo Component!

Todo Component

And we start with some clean up:

import { Component, Prop, State } from '@stencil/core';
import { StoreManager } from '../store/store-manager';

@Component({
  tag: 'my-todo'
})
export class Todo {
  @Prop({ connect: 'store-manager' }) storeManagerLoader;
  @State() storeManager: StoreManager;

  @Prop() value: string;
  @Prop() id: string;
  @State() isEditable = false;

  componentWillLoad() {
    this.getStoreService();
  }
  ...
}

The Event and EventEmitter aren't required anymore because we will ask the StoreManager to update the Redux Store.
Similarly to the TodoList Component, we acquire the StoreManager by using the getStoreService in the componentWillLoad hook.

We can update our old methods:

  removeThisTodo = () => {
    this.storeManager.delete(this.id);
  }

  updateThisTodo(id, value) {
    this.storeManager.update(id, value);
  }

We don't emit events anymore, all we do now is passing the order to the storeManager.

And Voila!

We have our TODO back as it should:

stencil todo final result

Conclusion

Redux makes our life easier.
Using multiple Event Emitters and Listeners in a big application can quickly lead to a mess. At the moment, Dependency Injection with connect is quite tricky. We can't use it with the Redux library as it is and we need a StoreManager component, which brings up a new point. The current and upcoming libraries might need some reworks in order to be Stencil-compliant.

Creating a TODO application using Stencil

Understanding the Manifest of an Ionic PWA in One Go

Using Ionic With Capacitor in One Go

Stay up to date


Join over 4000 other developers who already receive my tutorials and their source code out of the oven + other free stuff!