Giter Club home page Giter Club logo

redux-immutablejs's Introduction

redux-immutablejs

Redux & Immutable integration

This is a small library that aims to provide integration tools between Redux & ImmutableJs that fully conforms Redux actions & reducers standards.

  1. An alternative to combineReducers that supports ImmutableJs for store initial state.
  2. An optional handler map reducer creator with immutable support.

Setup

Initial State

Using combineReducers it is possible to provide createStore with initial state using Immutable Iterable type, i.e:

import { createStore } from 'redux';
import { combineReducers } from 'redux-immutablejs';

import Immutable from 'immutable';
import * as reducers from './reducers';

const reducer = combineReducers(reducers);
const state = Immutable.fromJS({});

const store = reducer(state);
export default createStore(reducer, store);

Immutable Handler Map reducer creator

Using createReducer is an optional function that creates a reducer from a collection of handlers. In addition to getting rid of the switch statement, it also provides the following benefits:

  1. If the given initialState type is mutated, it will get converted to an immutable type.
  2. An error is produced in case a reducer handler returns a mutated state (not recommended but this behavior can be disabled)
import { createReducer } from 'redux-immutablejs'
const initialState = Immutable.fromJS({ isAuth: false })

/**
 * Reducer domain that handles authentication & authorization.
 **/
export default createReducer(initialState, {
  [LOGIN]: (state, action) => state.merge({
    isAuth: true,
    token: action.payload.token
  }),

  [LOGOUT]: (domain) => domain.merge({
    isAuth: false,
    current_identity: {},
    token: undefined
  })
})

If you want to specify the Immutable type to be used for implicit conversion, pass an constructor function at the end:

export default createReducer([], {
  [ADD_STUFF]: (state, { stuff }) => state.add(stuff)
}, true, ::Immutable.OrderedSet);

Please note that this is optional and combineReducers should work just fine if you prefer the old switch way.

FAQ

How this library is different from 'redux-immutable' ?

This library doesn't dictate any specific reducer structure. While redux-immutable focuses on CRC, this library provides some conversion middlewares from FSA to CCA and vise versa. If you feel like going with Redux's vanilla is the right approach, then consider using our library.

redux-immutablejs's People

Contributors

angeloashmore avatar asaf avatar bzalasky avatar clkao avatar erikras avatar lsthornt avatar phallguy avatar stevenlangbroek avatar wpcarro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-immutablejs's Issues

Discrepancy between github repo and published npm version

There seems to be a difference between the contents of /src/utils/combineReducers.js as published on github and what is pulled down from a fresh npm install.

The differences start almost immediately, here's the current github version of the file from https://github.com/indexiatech/redux-immutablejs/blob/master/src/utils/combineReducers.js

import Immutable from 'immutable';

// TODO need to find a way to reference Redux's init for compatability
const ActionTypes = { INIT: 'INIT' };
const isImmutable = (obj) => {
  return Immutable.Iterable.isIterable(obj);
};

/* eslint-disable no-console */

function getErrorMessage(key, action) {
  var actionType = action && action.type;
  var actionName = actionType && `"${actionType.toString()}"` || 'an action';

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  );
}

function verifyStateShape(initialState, currentState) {
  var reducerKeys = currentState.keySeq();

  if (reducerKeys.size === 0) {
    console.error(
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
    return;
  }

  if (!isImmutable(initialState)) {
    console.error(
      'initialState has unexpected type of "' +
      ({}).toString.call(initialState).match(/\s([a-z|A-Z]+)/)[1] +
      '". Expected initialState to be an instance of Immutable.Iterable with the following ' +
      `keys: "${reducerKeys.join('", "')}"`
    );
    return;
  }

  const unexpectedKeys = initialState.keySeq().filter(
   key => reducerKeys.indexOf(key) < 0
  );

  if (unexpectedKeys.size > 0) {
    console.error(
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" in initialState will be ignored. ` +
      `Expected to find one of the known reducer keys instead: "${reducerKeys.join('", "')}"`
    );
  }
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */

export default function combineReducers(reducers) {
  reducers = isImmutable(reducers) ? reducers : Immutable.fromJS(reducers);
  const finalReducers = reducers.filter(v => typeof v === 'function');

  finalReducers.forEach((reducer, key) => {
    if (typeof reducer(undefined, { type: ActionTypes.INIT }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      );
    }

    var type = Math.random().toString(36).substring(7).split('').join('.');
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      );
    }
  });

  var defaultState = finalReducers.map(r => undefined);
  var stateShapeVerified;

  return function combination(state = defaultState, action) {
    var dirty = false;
    var finalState = finalReducers.map((reducer, key) => {
      var oldState = state.get(key);
      var newState = reducer(oldState, action);
      dirty = dirty || (oldState !== newState)
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState;
    });

    if ((
      // Node-like CommonJS environments (Browserify, Webpack)
      typeof process !== 'undefined' &&
      typeof process.env !== 'undefined' &&
      process.env.NODE_ENV !== 'production'
    ) ||
      // React Native
      typeof __DEV__ !== 'undefined' &&
      __DEV__ // eslint-disable-line no-undef
    ) {
      if (!stateShapeVerified) {
        verifyStateShape(state, finalState);
        stateShapeVerified = true;
      }
    }

    return (dirty) ? finalState : state;
  };
}

Which is pretty different from what i'm getting locally from npm

import Immutable from 'immutable';
// TODO need to find a way to reference Redux's init for compatability
const ActionTypes = { INIT: 'INIT' };
const isImmutable = (obj) => {
  return Immutable.Iterable.isIterable(obj);
};

/* eslint-disable no-console */

function getUndefinedStateErrorMessage(key, action) {
  var actionType = action && action.type;
  var actionName = actionType && `"${actionType.toString()}"` || 'an action';

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  );
}

function getUnexpectedStateKeyWarningMessage(inputState, outputState, action) {
  var reducerKeys = Object.keys(outputState);
  var argumentName = action && action.type === ActionTypes.INIT ?
    'initialState argument passed to createStore' :
    'previous state received by the reducer';

  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
  }

  if (!isImmutable(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
  }

  var unexpectedKeys = inputState.keySeq().filter(
    key => reducerKeys.indexOf(key) < 0
  );

  if (unexpectedKeys.size > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

function assertReducerSanity(reducers) {
  reducers.keySeq().forEach(key => {
    var reducer = reducers.get(key);
    var initialState = reducer(undefined, { type: ActionTypes.INIT });

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      );
    }

    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      );
    }
  });
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */

export default function combineReducers(reducers) {
  let finalReducers = isImmutable(reducers) ? reducers : Immutable.fromJS(reducers);
  finalReducers = finalReducers.filter(v => typeof v === 'function');
  var sanityError;

  try {
    assertReducerSanity(finalReducers);
  } catch (e) {
    sanityError = e;
  }

  var defaultState = finalReducers.map(r => undefined);

  return function combination(state = defaultState, action) {
    if (sanityError) {
      throw sanityError;
    }

    var dirty = false;
    var finalState = finalReducers.map((reducer, key) => {
      var oldState = state.get(key);
      var newState = reducer(oldState, action);
      dirty = dirty || (oldState !== newState)
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState;
    });

    if (process.env.NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateKeyWarningMessage(state, finalState, action);
      if (warningMessage) {
        console.error(warningMessage);
      }
    }

    return (dirty) ? finalState : state;
  };
}

I'm wondering why if there was an update to the code that was not pushed to github as well? Also makes me wonder what other files are different or if this is an isolated case

Issues with initialState when using combineReducers

Similar to #10:

I am trying to init my store by calling a function initStore(initialState) like this (initialState is an Immutable.Map):

createStore = require('redux').createStore
combineReducers = require('redux-immutablejs').combineReducers

initStore = (initialState) ->
  mainReducer = combineReducers(reducers)
  store = createStore(mainReducer, initialState)

if I debug the last line, the store.getState() returns the default initialState of the reducer, not the initialState I am sending here to initStore(initialState).

It seems that combineReducers creates the reducers with their default initialState, which is expected, but calling redux's createStore with the initialState is not changing the state. What's the intended way to do so?

combineReducers to return Immutable.Record instead of Immutable.Map

Currently combineReducers({sub1, sub2}) returns an Immutable.Map. However, Immutable.js provides a native Record type which is more natural fit for holding data with specific, unchanging set of keys.

Here's the breakdown:

Option Immutable.Map Immutable.Record
Accessing sub-states state.get('sub1') state.sub1 - much cleaner (but state.get('sub1') also supported)
Creating inexistent sub-states Possible by mistake: state.set('foo', 5) Not allowed

I believe redux-immutablejs must be improved to return a Record instead of a Map by default. This will be fully backwards-compatible in all cases besides creating inexistent sub-states (which is a clear misuse of the combined reducers pattern).

By the way, it is already possible to pass a Record instance to combineReducers, but it requires non-trivial boilerplate code (which is most probably suboptimal as well):

import Immutable from 'immutable'
import {combineReducers} from 'redux-immutablejs'

import sub1 from './sub1'
import sub2 from './sub2'

class State extends Immutable.Record({
    sub1: null,
    sub2: null
}) {

    // The two methods below allow to trick combineReducers to accept this class
    // instead of Immutable.Map or plain object

    filter() {
        return this;
    }

    map(k, v) {
        return new State(Immutable.Map(this).map(k, v));
    }
}

export default combineReducers(new State({sub1, sub2}));

.map function always returns new object

This isn't necessarily a bug, but I just recently learned that ImmutableJs's .map method always returns a new object, thus breaking === equality tests to see if something has changed. This can be a big deal if you are using something like reselect to memoize your state so you don't trigger expensive re-renders when the state hasn't changed. (Immutable.is is one workaround, but that does a deep equality check, so it negates a lot of the benefit of using ImmutableJs in the first place)

Is there some way we could avoid using .map in combineReducers? I know it looks elegant and works well from a correctness point-of-view, but the practical benefit of using .set or .merge (and thus allowing simple === checks for state changes) could be worthwhile for users of this library.

Not installable from Git

lib/index.js does not exist in the git repo. Should it be shipped so people can install the package directly from github? It is very useful for testing PRs or testing latest versions in general.

OBSERVATION: fromJS in combineReducers not successfully converting to Map in node (but works in browser)

Observation

Problem

Note: using immutable ^3.8.1

Creating store works fine in browser.

But in node v9.2.0 test environment, combineReducers was complaining that finalReducers.filter is not a function.

TypeError: finalReducers.filter is not a function

at combineReducers (node_modules/redux-immutablejs/lib/utils/combineReducers.js:83:33)

Research

I instrumented combineReducers thus:

// redux-immutablejs/lib/utils/combineReducers.js
function combineReducers(reducers) {
  var finalReducers = isImmutable(reducers) ? reducers : _immutable2['default'].fromJS(reducers);
  console.log(`

    reducers was a ${typeof reducers}
    reducers ${isImmutable(reducers) ? 'is' : 'is not' } immutable
    reducers keys: ${Object.keys(reducers).length}

    finalReducers is a ${typeof finalReducers}
    finalReducers ${_immutable2['default'].Map.isMap(finalReducers) ? 'is' : 'is not'} a Map
    fromJS(finalReducers) is a ${
      _immutable2['default'].Map.isMap(
        _immutable2['default'].fromJS(reducers)
      )
      ? 'Map' 
      : _immutable2['default'].List.isList(
        _immutable2['default'].fromJS(reducers)
      )
      ? 'List'
      : 'not list or map?'
    }
    `      
  )
  finalReducers = finalReducers.filter(function (v) {
    return typeof v === 'function';
  });
// ...

Browser (Chrome) output:

      reducers was a object
      reducers is not immutable
      reducers keys: 29


      finalReducers is a object
      finalReducers is a Map

On node v9.2.0 Error

      reducers was a object
      reducers is not immutable
      reducers keys: 29


      finalReducers is a object
      finalReducers is not a Map

Work-around

I just convert to map before I pass into combineReducers

Naive suggestion

It must be something to do with the node environment (babel?). But perhaps for safety's sake:

var finalReducers = isImmutable(reducers) ? reducers : _immutable2['default'].fromJS(reducers);

could be changed to:

  var finalReducers = isImmutable(reducers) ? reducers : _immutable2['default'].Map(reducers);

redux 4.0

Has anyone tested redux 4.0 and redux-immutablejs?

Can not work with redux-router

reducers.js

import { combineReducers } from 'redux-immutablejs';
...
export const rootReducer = combineReducers({
  router: routerStateReducer,
  ...
});

configureStore.js

...
const initialState = rootReducer(Immutable.fromJS({}));
const logger = createLogger();
const finalCreateStore = compose(
    applyMiddleware(thunk, api, logger),
    reduxReactRouter({
        routes,
        createHistory
    })
)(createStore);

export default function configureStore(initialState) {
  return finalCreateStore(rootReducer, initialState);
};

There is something wrong, and the console in Chrome says

Uncaught TypeError: Cannot read property 'type' of undefined

Where type in

function routerStateReducer(state, action) {
  if (state === undefined) state = null;

  var _extends2;

  switch (action.type) { // error 
    case _constants.ROUTER_DID_CHANGE:
      return action.payload;
    case _constants.REPLACE_ROUTES:
      if (!state) return state;
      return _extends({}, state, (_extends2 = {}, _extends2[_constants.DOES_NEED_REFRESH] = true, _extends2));
    default:
      return state;
  }
}

I follow the example in README, but it did not work. I'm new to redux, could you tell me how to fix it.

npm package cannot be used with Babel 6

I recently upgraded to Babel 6 and noticed the following issues w/ your npm package:

/project/node_modules/redux-immutablejs/lib/index.js: Unknown option: /project/node_modules/redux-immutablejs/.babelrc.stage

It's generally considered bad practice to publish .babelrc as part of your npm packages. Instead, you should be using the files attribute in your package.json (https://docs.npmjs.com/files/package.json#files).

Alternatively, you can upgrade your package to Babel 6.

Consider deprecating in favour of redux-immutable

Original thread: gajus/redux-immutable#12 (comment)

@gaearon now that this update is out, can I ask you to replace redux-immutablejs with redux-immutable in http://redux.js.org/docs/introduction/Ecosystem.html#utilities?

The current implementation of redux-immutable is less opinionated (ironically, given the 1.0.0 version) and 2x faster (as asserted using [this primitive benchmark](https://github.com/gajus/redux-immutable/blob/5a5af3155073a4883f8cfc39a284e7f38d355dc8/benchmarks/> index.js)) than redux-immutablejs:

starting redux-immutablejs rootReducer iteration
target rootReducer iteration x 338,215 ops/sec ±1.68% (84 runs sampled)
starting redux-immutable rootReducer iteration
target rootReducer iteration x 721,491 ops/sec ±1.21% (89 runs sampled)

I will drop a message to @indexiatech suggesting:

  • to deprecate redux-immutablejs in favour of redux-immutable
  • suggest to release createReducer as a separate utility (which is the same route I am taking the Canonical Composition Pattern implementation)

Context:

I have recently released redux-immutable@2 that does no more and no less than allow state and initialState to be an instance of Immutable.Iterable.

I am moving all the Canonical Composition Pattern implementation under a different name that in turn will rely on redux-immutable. @asaf I'd like to suggest to do the same for createReducer.

I am happy to listen to your feedback should you have suggestions for redux-immutable improvement prior to deprecating redux-immutablejs.

TypeScript support?

HI all,

What about typescript support? Is there any plans to implement typings?

Upgrading to immutable 4

With immutable 4 in rc9...are there any plans to update this so users that use redux-immutablejs can upgrade?

understanding effective use of (use-cases for) redux-immutablejs

My apologies for asking a silly question. I am new to redux world and started exploring use of immutable data structures recently.
I don't think I correctly understand the use-cases for which I should use this library and for which I should not. Perhaps, I really need to understand benefits we get by defining whole state as immutable structure over defining the individual parts as immutable but wrapper state as object.

e.g.  If I have redux state as follows :
 {
   todos , //array of todos 
   visibleFilter //boolean
 }

Now, I tried to defined todos as Immutable.List with single todo as a Immutable.Map. I kept visibleFilter as boolean only. I kept wrapper state object as a plain old object too. Is this case a good candidate where I should use redux-immutablejs ?

If the final/wrapper redux state itself is an array then I can surely see that using redux-immutablejs to make it immutable List would be a great idea as structural sharing would come into play.

I am sure I am missing something here. It would be great If anyone could please explain this.

Having trouble providing initial state to reducers

I'm trying to write a reducer, but my tests keep failing.

Here's the reducer:

import Immutable from 'immutable';
import { createReducer } from 'redux-immutablejs';
import * as types from '../constants/CharacterActionTypes';

const initialState = Immutable.fromJS(localStorage.characters || []);

export const characters = createReducer(initialState, {
  [types.GET_CHARACTERS]: (state) => state
});

And my test:

import Immutable from 'immutable';
import { combineReducers } from 'redux-immutablejs';
import * as types from '../../constants/CharacterActionTypes';
import * as characters from '../CharacterReducers';

describe('Character reducers', () => {
  const testReducer = combineReducers(characters);
  const defaultState = Immutable.fromJS([
    {
      id: 1,
      name: 'Test Character'
    }
  ]);

  it(`should handle ${types.GET_CHARACTERS}`, () => {
    expect(testReducer(defaultState, {
      type: types.GET_CHARACTERS
    }).toJS()).toEqual(defaultState.toJS());
  });
});

That test fails with the following message:

Expected Object({ characters: [  ] }) to equal [ Object({ id: 1, name: 'Test Character' }) ].

I'm sure I'm just missing something obvious. Can you give me some idea of how to structure my reducer so this functions as expected?

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.