6

Angular 1 using redux architecture

 3 years ago
source link: http://brianyang.com/angular-1-using-redux-architecture/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Angular 1 using redux architecture

August 24, 2016 redux, angular


Lately I've been working on a React project, and while searching for a library to manage my application state I found redux.

In this post I assume you have some basic knowledge of the redux architecture. If you are new to redux I suggest viewing Dan Abramov tutorial on egghead and the great redux book: Complete Redux Book written by the guys from 500 Tech.

This article is heavily inspired by Nir Kaufman's workshop about angular 2 & redux.

You can find the final github repository here.

Why Redux?

Redux, as described on it's github repository is a "predictable state container for JavaScript apps". Which basically means that your entire app state is inside a single store, this allows you to save, regenerate, and interact with your entire application state from a single place.

One of the greatest pains in angular applications is managing state. Many times in real world scenarios it's really hard to follow the application flow, since different controllers may interact with multiple models, then some presentational components may have and manage their own state.

Redux main goal is to solve this issues by having a single point of truth using a single store for the entire application, as opposed to react flux (from where redux was heavily inspired) that have multiple stores.

Angular 1 and Redux

The use of redux and angular is mostly known from angular 2, but for those of you that still write your applications using Angular 1 this article will help you with the process of setting app your application.

This post will use angular 1.5 version that introduced us the component method, you can read about it in my previous article. We will also use webpack for bundling and transpiling our application. You can read more about it here.

Starting our project

I will use a small angular 1 starter project I created for this post, you can find it here.

Let's examine our project structure:

.├──client│├── app││├── actions / ││├──components / ││├──constants / ││├──containers / ││├──middlewares / ││├──reducers / ││├──app.component.js││├── app.html││├── app.js││└── app.scss│└── index.html├── gulpfile.babel.js├── karma.conf.js├── package.json├── spec.bundle.js├── webpack.config.js├── webpack.dev.config.js└── webpack.dist.config.js

Our entire code will reside inside the client/app folder and webpack will be kind enough to bundle our entry file app.js and create a single bundle file for it, automatically injecting it inside the index.html.

The new folders introduced when using redux are : reducers, actions, constants and middlewares let's see what they all about.

reducers

Each redux application is made using a single state, this state is made of multiple reducers which construct the final state object.

Each reducer function is accepting the previous state and the action as it's arguments. The reducer then transforms the state and returns a new state instance. It is very important not to mutate the state object in any way inside your reducers, they should always return a new instance of the state object. The reducer functions should always be synchronous and pure, meaning that given the same inputs they will always produce the same output.

actions

In order to change the state our components will dispatch actions, those are simply containers for payloads you want to pass to the store. The only way your application will interact with the store is using actions, each action should have a type property and a payload property.

The actions dispatched are passed to the store in order to calculate the new app state. It is considered a good practice to write Action Creators for creating the action objects, these are simply functions that return the action object. The actions folder will contain the action creators of our app.

middlewares

Since our reducers are always synchronous we use middlewares to perform async requests. A middleware function is basically function that receives a store as argument, it then returns a next function which receives an action object as argument and than calls the next function after finished with the async job.

var middleware = function(store) {
        return function(next) {
                return function(action) { // some async code action.payload = { data: asyncResponse }; next(action) } } } // or using ES6 arrow functions const middleware = store => next => action => { next(action); };

constants

Since we will be using a lot of action types, it is considered a good practice to store your action names in a separate file and reference them when needed.

Our first store

We will start with creating our application store, first create a new index.js file inside the reducers folder:

import {
    combineReducers
} from 'redux';
import {
    TodosReducer
} from './todos.reducer';
export const RootReducer = combineReducers({
    todos: TodosReducer
});

Note that we are using combineReducers function, this helper function from redux allowing to construct our single store from multiple state objects, it is extremely useful for large applications, so you can manage your store using smaller reducer files, each responsible for it's own property on the store.

We declared a todos reducer on our root reducer, this will hold our todos list. It will be a simple array of strings.

Now we can create our TodosReducer file, inside the reducer folder create a new file named todos.reducer.js:

import {
    TODOS
} from '../constants/todos';
const initialState = [];
export function TodosReducer(state = initialState, action) {
    switch (action.type) {
        case TODOS.ADD_TODO:
            return [...state, action.payload];
        case TODOS.REMOVE_TODO:
            return [...state.slice(0, action.payload), ...state.slice(action.payload + 1)];
        default:
            return state;
    }
}

In the todos reducer we are creating initial state, so when the app bootstraps it will use this state object as a default, later we can even bootstrap the application using a previously saved state, cool huh?

The TodosReducer is responsible for adding a todo for the todos list or removing it based on the id passed with the action. Note that we always return a new state object using methods like array destructuring and object assign. You should never mutate the app state, and always return a new instance of it(You can check immutablejs to help you with keeping the state unmutated).

We are also using the constants file in this reducer, let's create him as-well inside the constants folder:

// constants/todos.js export const TODOS = { ADD_TODO: 'ADD_TODO', REMOVE_TODO: 'REMOVE_TODO' };

Last thing we need to write is our action creator, inside the actions folder create a new file named todo.actions.js:

import {
    TODOS
} from '../constants/todos';

function addTodo(todo) {
    return {
        type: TODOS.ADD_TODO,
        payload: todo
    }
}

function removeTodo(index) {
    return {
        type: TODOS.REMOVE_TODO,
        payload: index
    };
}
export default {
    addTodo,
    removeTodo
};

Here we are exporting two functions, one for each of our cases: add and remove. Our component controllers will interact with the state using those functions.

Integrate the root reducer to angular

For implementing the redux store to our angular application we will use a great helper service called angular-redux.

npm install ng - redux--save

Open the app.js file and modify it as follows:

import angular from 'angular';
import uiRouter from 'angular-ui-router';
import ngRedux from 'ng-redux';
import AppComponent from './app.component';
import NavigationComponent from './components/navigation/navigation';
import HomeComponent from './containers/home/home'; // import the root reducer from reducers folder import { RootReducer } from './reducers'; // import our default styles for the whole application import 'normalize.css'; import 'bootstrap/dist/css/bootstrap.css'; angular .module('app', [ uiRouter, ngRedux, NavigationComponent.name, HomeComponent.name ]) .config(($locationProvider, $stateProvider, $urlRouterProvider, $ngReduxProvider) => { "ngInject"; // Define our app routing, we will keep our layout inside the app component // The layout route will be abstract and it will hold all of our app views $stateProvider .state('app', { url: '', abstract: true, template: '<app></app>' }) // Dashboard page to contain our goats list page .state('app.home', { url: '/home', template: '<home></home>' }); $urlRouterProvider.otherwise('/home'); // create the root store using ng-redux $ngReduxProvider.createStoreWith(RootReducer); }) .component('app', AppComponent);

After importing the ng-redux module to our app we are injecting $ngReduxProvider, which will register our RootReducer with the angular application. We can also pass a second argument to the createStoreWith that will hold an array of the middlewares functions as described in the previous section.

Now let's start our application to see if we didn't mess up anything yet:

Navigate to http://localhost:3000/ and you shall see the default html components (navigation and home component).

Now we will dispatch some events and listen to their changes now.

Open the containers/home/home.html file, our next step is to add some basic markup for the todo application:

& lt;
div class = "container" & gt; & lt;
div class = "row" & gt; & lt;
div class = "col-md-6" & gt; & lt;
h1 & gt;
Todos & lt;
/h1> <div class="input-group"> <input type="text" class="form-control" placeholder="Add Todo" ng-model="$ctrl.todo"> <span class="input-group-btn"> <button class="btn btn-primary" type="button" ng-disabled="!$ctrl.todo" ng-click="$ctrl.submitTodo($ctrl.todo)">Save</button & gt; & lt;
/span> </div & gt; & lt;
!--/input-group --> <hr> <ul class="list-unstyled"> <li class="ui-state-default" ng-repeat="todo in $ctrl.todos"> <div class="checkbox"> <label> <input type="checkbox" ng-click="$ctrl.removeTodo($index)"> {{todo}} </label & gt; & lt;
/div> </li & gt; & lt;
/ul> </div & gt; & lt;
/div> </div & gt;

After the component html file is changed, let's implement the controller methods. Open containers/home/home.controller.js, we first need a way to access the dispatch function from our store and subscribe to the state changes, luckily ng-redux got us covered.

import TodoActions from '../../actions/todo.actions';
export default class HomeController {
    constructor($ngRedux) {
        this.todo = '';
        this.unsubscribe = $ngRedux.connect(this.mapStateToThis, TodoActions)(this);
    }
    submitTodo() {
        this.addTodo(this.todo);
        this.todo = '';
    }
    $onDestroy() {
        this.unsubscribe();
    }
    mapStateToThis(state) {
        return {
            todos: state.todos
        };
    }
}
HomeController.$inject = ["$ngRedux"];

Let's break this code snippet:

this.unsubscribe = $ngRedux.connect(this.mapStateToThis, TodoActions)(this);

The connect function allows our controller to subscribe to the state changes, note that we are passing to it 2 arguments:

mapStateToThis allows us to select specific part of the state and bind it to our controller. For our todo app we need only the todos array, for more complex applications you can abstract the controller from interacting with nested state trees an simply receive the properties it needs.

 mapStateToThis(state) {
     return {
         todos: state.todos
     };
 }

The mapStateToThis function returns a plain js object that will be available on our controller class.

The second argument passed to the connect function is the ActionCreator functions we created before, they will also be available on the component's controller.

$onDestroy() {
    this.unsubscribe();
}

Here we are using angular component hook to unsubscribe the state watcher when the $scope is destroyed. It's very important to unsubscribe any watchers you are creating on angular when the $scope is destroyed.

submitTodo() {
    this.addTodo(this.todo);
    this.todo = '';
}

The next method simply calls the action creator method and passes the todo model to the creator function. After the action fired we reset the todo model.

In order to remove the task we call the removeTodo action directly from the view and passing the ng-repeat index as the id:

Summary

Redux is a great solution for managing your application state and reproduce user actions easily. There are a lot of great dev tools for the redux eco system including Redux-dev-tools (super cool debugging tool create by redux writer) and hot module swapping.

There are a lot of great resources there about using redux with react and angular 2, but not a lot for angular 1 developers. I hope this article will help you with setting up your angular & redux projects.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK