angular-redux / ng-redux

Angular bindings for Redux
MIT License
1.16k stars 177 forks source link

ng-redux

Angular bindings for Redux.

For Angular 2+ see angular-redux/store -- made by the same people that started ng-redux.

build status npm version Conventional Commits

ngRedux lets you easily connect your angular components with Redux.

Table of Contents

Installation

npm

npm install --save ng-redux

bower (deprecated)

Warning! Starting with 4.0.0, we will no longer be publishing new releases on Bower. You can continue using Bower for old releases, or point your bower config to the UMD build hosted on unpkg that mirrors our npm releases.

{
  "dependencies": {
    "ng-redux": "https://unpkg.com/ng-redux/umd/ng-redux.min.js"
  }
}

Add the following script tag to your html:

<script src="https://github.com/angular-redux/ng-redux/raw/master/bower_components/ng-redux/dist/ng-redux.js"></script>

Or directly from unpkg

<script src="https://unpkg.com/ng-redux/umd/ng-redux.min.js"></script>

Quick Start

Initialization

There are three ways to instantiate ngRedux:

  1. createStoreWith
  2. provideStore
  3. createStore
createStoreWith

You can either pass a function or an object to createStoreWith.

With a function:

import reducers from './reducers';
import { combineReducers } from 'redux';
import loggingMiddleware from './loggingMiddleware';
import ngRedux from 'ng-redux';

angular.module('app', [ngRedux])
.config(($ngReduxProvider) => {
    let reducer = combineReducers(reducers);
    $ngReduxProvider.createStoreWith(reducer, ['promiseMiddleware', loggingMiddleware]);
  });

With an object:

import reducers from './reducers';
import loggingMiddleware from './loggingMiddleware';
import ngRedux from 'ng-redux';
import reducer3 from './reducer3';

angular.module('app', [ngRedux])
.config(($ngReduxProvider) => {
    reducer3 = function(state, action){}
    $ngReduxProvider.createStoreWith({
        reducer1: "reducer1",
        reducer2: function(state, action){},
        reducer3: reducer3
     }, ['promiseMiddleware', loggingMiddleware]);
  });

In this example reducer1 will be resolved using angular's DI after the config phase.

provideStore

You can pass an already existing store to ngRedux using provideStore:

import reducers from './reducers';
import { createStore, combineReducers } from 'redux';
import ngRedux from 'ng-redux';

const reducer = combineReducers(reducers);
const store = createStore(reducer);

angular.module('app', [ngRedux])
.config(($ngReduxProvider) => {
    $ngReduxProvider.provideStore(store);
  });
createStore

createStore allows you take full control over the store creation. This is handy if you want to control the order of enhancers by your self. It takes a function that gets middlewares and enhancers from ngRedux as a parameters. Note that middlewares provided by ngRedux needs to be last ones.

import reducers from './reducers';
import { createStore, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import ngRedux from 'ng-redux';

const reducer = combineReducers(reducers);

angular.module('app', [ngRedux])
.config(($ngReduxProvider) => {
    $ngReduxProvider.createStore((middlewares, enhancers) => {
        return createStore(
          reducer,
          {},
          compose(applyMiddleware(thunk, ...middlewares), ...enhancers)
        )
    });
  });

Usage

Using controllerAs syntax

import * as CounterActions from '../actions/counter';

class CounterController {
  constructor($ngRedux, $scope) {
    /* ngRedux will merge the requested state's slice and actions onto this,
    you don't need to redefine them in your controller */

    let unsubscribe = $ngRedux.connect(this.mapStateToThis, CounterActions)(this);
    $scope.$on('$destroy', unsubscribe);
  }

  // Which part of the Redux global state does our component want to receive?
  mapStateToThis(state) {
    return {
      value: state.counter
    };
  }
}
<div>
    <p>Clicked: {{counter.value}} times </p>
    <button ng-click='counter.increment()'>+</button>
    <button ng-click='counter.decrement()'>-</button>
    <button ng-click='counter.incrementIfOdd()'>Increment if odd</button>
    <button ng-click='counter.incrementAsync()'>Increment Async</button>
</div>

API

createStoreWith(reducer, [middlewares], [storeEnhancers], [initialState])

Creates the Redux store, and allow connect() to access it.

Arguments:

connect(mapStateToTarget, [mapDispatchToTarget])(target)

Connects an Angular component to Redux.

Arguments

You then need to invoke the function a second time, with target as parameter:

e.g:

connect(this.mapState, this.mapDispatch)(this);
//Or
connect(this.mapState, this.mapDispatch)((selectedState, actions) => {/* ... */});

Returns

Returns a Function allowing to unsubscribe from further store updates.

Remarks

Store API

All of redux's store methods (i.e. dispatch, subscribe and getState) are exposed by $ngRedux and can be accessed directly. For example:

$ngRedux.subscribe(() => {
    let state = $ngRedux.getState();
    //...
})

This means that you are free to use Redux basic API in advanced cases where connect's API would not fill your needs.

Dependency Injectable Middleware

You can use angularjs dependency injection mechanism to resolve dependencies inside a middleware. To do so, define a factory returning a middleware:

function myInjectableMiddleware($http, anotherDependency) {
    return store => next => action => {
        //middleware's code
    }
}

angular.factory('myInjectableMiddleware', myInjectableMiddleware);

And simply register your middleware during store creation:

$ngReduxProvider.createStoreWith(reducers, [thunk, 'myInjectableMiddleware']);

Middlewares passed as string will then be resolved throught angular's injector.

Config

Debouncing the digest

You can debounce the digest triggered by store modification (usefull in huge apps with a lot of store modifications) by passing a config parameter to the ngReduxProvider.

import angular from 'angular';

angular.module('ngapplication').config(($ngReduxProvider) => {
  'ngInject';

  // eslint-disable-next-line
  $ngReduxProvider.config.debounce = {
    wait: 100,
    maxWait: 500,
  };
});

This will debounce the digest for 100ms with a maximum delay time of 500ms. Every store modification within this time will be handled by this delayed digest.

lodash.debounce is used for the debouncing.

Routers

See redux-ui-router to make ng-redux and UI-Router work together.
See ng-redux-router to make ng-redux and angular-route work together.

Using DevTools

There are two options for using Redux DevTools with your angular app. The first option is to use the [redux-devtools package] (https://www.npmjs.com/package/redux-devtools), and the other option is to use the [Redux DevTools Extension] (https://github.com/zalmoxisus/redux-devtools-extension#usage). The Redux DevTools Extension does not require adding the react, react-redux, or redux-devtools packages to your project.

To use the redux-devtools package, you need to install react, react-redux and redux-devtools as development dependencies.

[...]
import { devTools, persistState } from 'redux-devtools';
import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react';
import React, { Component } from 'react';

angular.module('app', ['ngRedux'])
  .config(($ngReduxProvider) => {
      $ngReduxProvider.createStoreWith(rootReducer, [thunk], [devTools()]);
    })
  .run(($ngRedux, $rootScope, $timeout) => {
    React.render(
      <App store={ $ngRedux }/>,
      document.getElementById('devTools')
    );

    //To reflect state changes when disabling/enabling actions via the monitor
    //there is probably a smarter way to achieve that
    $ngRedux.subscribe(() => {
        $timeout(() => {$rootScope.$apply(() => {})}, 100);
    });
  });

  class App extends Component {
  render() {
    return (
      <div>
        <DebugPanel top right bottom>
          <DevTools store={ this.props.store } monitor = { LogMonitor } />
        </DebugPanel>
      </div>
    );
  }
}
<body>
    <div ng-app='app'>
      [...]
    </div>
    <div id="devTools"></div>
</body>

To use the Redux DevTools extension, you must first make sure that you have installed the Redux DevTools Extension.

angular.module('app', ['ngRedux'])
  .config(($ngReduxProvider) => {
      $ngReduxProvider.createStoreWith(rootReducer, [thunk], [window.__REDUX_DEVTOOLS_EXTENSION__()]);
    })
  .run(($ngRedux, $rootScope, $timeout) => {
    //To reflect state changes when disabling/enabling actions via the monitor
    //there is probably a smarter way to achieve that
    $ngRedux.subscribe(() => {
        $timeout(() => {$rootScope.$apply(() => {})}, 100);
    });
  });

Additional Resources