Giter Club home page Giter Club logo

redux-data-structures's Introduction

Redux Data Structures

Introduction

Redux Data Structures is a library of reducer makers.

Reducer makers help create common reducers like counters, maps, lists (queues, stacks), sets, etc. Most application states can be built by combining a handful of these standardized building blocks.

Redux Data Structures was developed for Redux, but does not depend on it. It can actually be used with any reactive state container, even a custom one; Redux Data Structures doesn't have any dependency.

Getting Started

npm install --save redux-data-structures

Here's an example from the Redux README, rewritten with Redux Data Structures:

import { createStore } from 'redux';
import { counter } from 'redux-data-structures';

const myCounter = counter({
  incrementActionTypes: ['INCREMENT'],
  decrementActionTypes: ['DECREMENT'],
});

const store = createStore(myCounter);

store.subscribe(() => { console.log(store.getState()); });

store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

Configuring Data Structures

Here's a more advanced example--with the same reducer maker--of a counter from 10 to 0, decreasing as a function of the action payload, then reset, representing life points for example:

import { createStore } from 'redux';
import { counter } from 'redux-data-structures';

const lifePoints = counter({
  initialState: 10,
  decrementActionTypes: ['PUNCH', 'KICK'],
  decrement: action => action.value,
  min: () => 0, // action => number
  resetActionTypes: ['INSERT_COIN'],
});

const store = createStore(lifePoints);

store.subscribe(() => { console.log(store.getState()); });

store.dispatch({ type: 'PUNCH', value: 5 });
// 5
store.dispatch({ type: 'KICK', value: 7 });
// 0
store.dispatch({ type: 'INSERT_COIN' });
// 10

Combining Data Structures

Let's build a classic todo app with Redux Data Structures:

import { createStore, combineReducers } from 'redux';
import { map, set, value } from 'redux-data-structures';

const todos = map({
  addActionTypes: ['ADD_TODO'],
  removeActionTypes: ['REMOVE_TODO'],
});

const completedTodos = set({
  toggleActionTypes: ['TOGGLE_TODO'],
  removeActionTypes: ['REMOVE_TODO'],
  keyGetter: action => action.payload.id,
});

const visibilityFilter = value({
  initialState: 'SHOW_ALL',
  setActionTypes: ['SET_VISIBILITY_FILTER'],
  valueGetter: action => action.payload.filter,
});

const rootReducer = combineReducers({
  todos,
  completedTodos,
  visibilityFilter,
});

const store = createStore(rootReducer);

That's all for the store! We've relied heavily on the reducer makers' default options, which presume that:

  1. actions adhere to the Flux Standard Action (actions are plain Javascript object with a type and payload properties),
  2. and Todos are identified by an id property, used as a key in the todos map (and the completetedTodos set).

Now let's subscribe to the store and dispatch a few actions:

store.subscribe(() => { console.log(JSON.stringify(store.getState(), null, 2)); });

store.dispatch({
  type: 'ADD_TODO',
  payload: {
    id: 0,
    text: 'Go fishing',
  },
});
// {
//   "todos": {
//     "byId": {
//       "0": {
//         "id": 0,
//         "text": "Go fishing"
//       }
//     },
//     "allIds": [
//       0
//     ]
//   },
//   "completedTodos": {},
//   "visibilityFilter": "SHOW_ALL"
// }

Notice that todos is normalized, for the reasons explained in the Redux documentation.

store.dispatch({
  type: 'TOGGLE_TODO',
  payload: { id: 0 },
});
// {
//   "todos": {
//     "byId": {
//       "0": {
//         "id": 0,
//         "text": "Go fishing"
//       }
//     },
//     "allIds": [
//       0
//     ]
//   },
//   "completedTodos": {
//     "0": true
//   },
//   "visibilityFilter": "SHOW_ALL"
// }

Compared to the original Redux Todo example, we've separated the Todo items (id, text) from their completion state. If needed, they could be combined with a selector.

store.dispatch({
  type: 'SET_VISIBILITY_FILTER',
  payload: { filter: 'SHOW_COMPLETED' },
});
// {
//   "todos": {
//     "byId": {
//       "0": {
//         "id": 0,
//         "text": "Go fishing"
//       }
//     },
//     "allIds": [
//       0
//     ]
//   },
//   "completedTodos": {
//     "0": true
//   },
//   "visibilityFilter": "SHOW_COMPLETED"
// }
store.dispatch({
  type: 'REMOVE_TODO',
  payload: { id: 0 },
});
// {
//   "todos": {
//     "byId": {},
//     "allIds": []
//   },
//   "completedTodos": {},
//   "visibilityFilter": "SHOW_COMPLETED"
// }

The REMOVE_TODO action is reduced both by the todos map and the completedTodos set.

Data Structures

So far, the following data structures have been implemented (corresponding action types are indicated in parentheses):

  • Boolean (set to true, set to false, toggle)
  • Counter (increment, decrement)
  • List (queue or stack: enqueue, dequeue, push, pop)
  • Map (add, remove, change)
  • Set (add, remove, toggle)
  • Value (set)

All data structures can be reset to their initial state, and, if applicable (for lists, maps, and sets), emptied.

API

Each reducer maker is a higher-order function of a single options object and returns a reducer:

{ ...options } => (state, action) => state

For each reducer maker, we describe below how the options object is destructured, its default property values, and how some specific properties are used.

Defaults can--and in a lot of cases should--be overridden.

Each category of actions, e.g., decrementActionTypes, is an array of action types (i.e., strings), so that several action types can have the same result (cf. Configuring Data Structures, above, where both PUNCH and KICK decrement lifePoints).

Boolean

{
  initialState = false,
  trueActionTypes = [],
  additionalConditionToTrue = () => true,
  falseActionTypes = [],
  additionalConditionToFalse = () => true,
  toggleActionTypes = [],
  resetActionTypes = [],
}

additionalConditionToTrue and additionalConditionToFalse are functions of action and are used as such:

// ...
if (trueActionTypes.includes(action.type) && additionalConditionToTrue(action)) {
  return true;
} else if (falseActionTypes.includes(action.type) && additionalConditionToFalse(action)) {
  return false;
}
// ...

The default () => true is equivalent to no additional condition.

Counter

{
  initialState = 0,
  incrementActionTypes = [],
  increment = () => 1,
  max,
  decrementActionTypes = [],
  decrement = () => 1,
  min,
  resetActionTypes = [],
}

increment, decrement, max, and min are functions of action. If max is undefined, it is not enforced. Same for min.

List

{
  initialState = [],
  enqueueActionTypes = [],
  dequeueActionTypes = [],
  pushActionTypes = [],
  popActionTypes = [],
  itemGetter = action => action.payload,
  resetActionTypes = [],
  emptyActionTypes = [],
}

A list can be used as a queue or stack. enqueueActionTypes and pushActionTypes add items to the list, using the itemGetter. The default itemGetter adds the Flux Standard Action payload to the list.

Map

{
  initialState = {
    byId: {},
    allIds: [],
  },
  addActionTypes = [],
  changeActionTypes = [],
  removeActionTypes = [],
  keyGetter = action => action.payload.id,
  itemGetter = action => ({...action.payload}),
  itemModifier = (item, action) => ({...item, ...action.payload}),
  resetActionTypes = [],
  emptyActionTypes = [],
}

map uses the normalized state shape recommended by Redux, as can be seen from the default initialState. Warning: if you overwrite initialState, use the same format!

The default keyGetter assumes that the action payload has an id property. The default itemModifier overwrites the item's properties (but does not delete the ones that have disappeared in the new action payload).

Set

{
  initialState = {},
  addActionTypes = [],
  removeActionTypes = [],
  toggleActionTypes = [],
  keyGetter = action => action.payload,
  resetActionTypes = [],
  emptyActionTypes = [],
}

In Redux Data Structures, a set's state is a plain Javascript object with boolean properties, i.e. if and only if key is in the set, key is a property of state whose value is true. Example:

{ key: true }

When a key is removed from the set, the corresponding property is deleted from the state object:

{}

Value

{
  initialState = null,
  setActionTypes = [],
  valueGetter = action => action.payload,
  resetActionTypes = [],
}

value is the simplest data structure (to the extent that calling it a data structure is arguable).

Performance

Redux Data Structures doesn't focus on performance, but on developer productivity. In most cases, performance won't be an issue. If it is, please write an issue or submit a pull request.

Contributing

The code is written in modern Javascript, transpiled with Babel, using Jest for tests. Pull requests are welcome.

License

MIT

Author

Adrien Trouillaud, Codology.net

redux-data-structures's People

Contributors

adrienjt 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

redux-data-structures's Issues

Feature: "add all" action types for maps

Right now there is no simple way to add multiple items at once to a map. We have to dispatch an add action for each element when we want to add a list of items.

It would be nice to have a addAllActionTypes option that would iterate on the action's payload and add all items. This could also need a collectionGetter option.

For lists, since you're using Array.prototype.concat, the add actions work for adding both single and multiple items.

Example of Simple Value Map

A pattern I often find myself using, particularly for things like user data or the like, is a simple key: value map. In this instance the map() only really adds additional boilerplate while dealing with ids and such. After some tinkering I came up with a simple enough solution leveraging the existing value() data structure. While nothing amazingly original I thought maybe adding an example to the readme/docs for this use case could be useful. Example:

import { combineReducers, createStore } from 'redux';
import { value } from 'redux-data-structures';

const name = value({
  setActionsTypes: ['UPDATE_NAME']
});

const email = value({
  setActionsTypes: ['UPDATE_EMAIL']
});

const reducers = combineReducers({
  user: {
    name,
    email
  }
})

const store = createStore(reducers);

store.subscribe(() => { console.log(store.getState()); });

store.dispatch({ type: 'UPDATE_NAME', payload: 'FirstName LastName' });
// {
//   "user": {
//     "name": "FirstName LastName",
//     "email": null
//   }
// }
store.dispatch({ type: 'UPDATE_EMAIL', payload: '[email protected]' });
// {
//   "user": {
//     "name": "FirstName LastName",
//     "email": "[email protected]"
//   }
// }

Food for thought

Hey guys,
I noticed this mentioned in a react newsletter and it looks similar to an approach I took with moving our app to redux.

Couple of things I didn't like about the default redux code found in tutorials:

  • loads of uniquely named actions (ADD_TODO) as opposed to namespacing (.ADD)
  • non-reuse of reducers and constantly creating more of them

I came up with following solution:

  • reducer factories (this is what you're doing)
  • utilizing namespace and the same action names across same reducers, jus twith a different namespace (this is what redux already offers, but I don't see it used often enough)
  • reducer factories create action trees (this is the food for thought)

Let me give you an example, with a singleton reducer (it just holds a value):

export default ((namespace, initial = null) => {
  const initialState = initial;
  var namespace;

  const handler = (state = initialState, action) => {
    const [action_namespace, action_type] = action.type.split('/');

    if (namespace != action_namespace) {
      return state;
    }

    switch (action_type) {
      case 'set':
        return action.payload;
      default:
        return state;
    }
  };

  let store;
  handler.__init = (_store, _namespace) => {
    store = _store;
    namespace = _namespace;
  };

  handler.__actions = {
    'set': (value) => {
      store.dispatch({
        type: namespace+'/set',
        payload: value
      });
    }
  };

  return handler;
});

Then I create the store like this:

import {
  createStore,
  combineReducers,
} from 'redux';

import {each} from 'lodash-es';
import {pluralize, camelize} from 'inflection';

import singletonReducer from '../reducers/singletonReducer';
import mapActions from './mapActions';


// This allows us to easily call actions on reducers, like so:
// Actions.task.set() and
const mapActions = (map, store, actions) => {

  // set actions for each store
  each(map, (v, k) => {
    if (v.__init) {
      v.__init(store, k);
      actions[k] = v.__actions;
    }
  });
};


const configureStore = (map, initialState = {}, actions = {}) => {
  const reducer = combineReducers(map);

  // pre-fill computer values
  initialState = wrappedReducer(initialState, {type: 'initComputed'});

  const store = createStore(
    reducer,
    initialState,
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  );
  mapActions(map, store, actions);

  return store;
}

export default ({
  actions = {},
  state = window.__REDUX_INIT_CONTEXT,
} = {}) => {
  const map = {
    currentProjectId: singletonReducer(),
  };

  const store = configureStore(
    map,
    state,
    actions,
    globalReducers
  );

  return {
    redux: store,
    store: new Store(store, actions),
    actions: actions,
  };
};

Then I expose actions as window.Actions.

When I do that, I have a globally available map of actions, that is constructed by the reducers that I setup, so there is zero config, no need to remember or lookup action names.

Its all under Actions.<reducer>.<action>(...) and you can call it to dispatch action like that.

This is what I came up with in the first weeks and its super easy to use. You have far more experience so I bet you'll be able to come up with something smarter and cleaner if it inspires you.

Put getters together with action types

If I have two action types with different payload structure I have to describe the types and functions for selecting the data from these action in different places

const todos = map({
  addActionTypes: ['ADD_TODO', 'ADD_NOTE_WITH_TODO'],
  keyGetter: action => {
    if (action.type === 'ADD_NOTE_WITH_TODO') {
      return action.payload.note.todo.id;
    }

    if (action.type === 'ADD_TODO') {
      return action.payload.id;
    }
  },
  itemGetter: action => {
    if (action.type === 'ADD_NOTE_WITH_TODO') {
      return action.payload.note.todo;
    }

    if (action.type === 'ADD_TODO') {
      return action.payload;
    }
  },
});

That looks pretty bad because we divide describing action type and related logic.

What about describing all related getter methods together with these types?

const todos = map({
  addActionTypes: [
    { 
      type: 'ADD_TODO' 
    },
    {
      type: 'ADD_NOTE_WITH_TODO',
      keyGetter: action => action.payload.note.todo.id,
      itemGetter: action => action.payload.note.todo,
    },
  ],
});

Issue Building With Create React App

When using redux-data-structures in a Create React App based system development works fine. However when creating a production build using yarn build an UglifyJS error pops up:

screen shot 2017-08-08 at 10 06 38 am

Looks like an issue with Uglification on the defaults set inside the function definitions.

Interestingly cloning the redux-data-structures repository into a sub-directory of src in the app, building it, and linking directly to es6/index.js works just fine for both development and production build.

TypeScript?

Do you plan supporting it (@types/redux-data-structures), rewrite the library on TS or accepting a pull request that refactors it on TS?

Import as Module

screen shot 2017-07-27 at 6 50 09 pm

Looks like when importing as a module src/ is used and there is possibly some type of babel transformation plugin being used that I don't have in my project.

Feature: make getters generator functions

What about an idea to make getter methods be a generator function? It can allow us to do this for example:

const todos = map({
  addActionTypes: ['ADD_FEW_TODOS'],
  *keyGetter(action) {
    yield* action.payload.map(todo => todo.id);
  },
  *itemGetter(action) {
    yield* action.payload;
  },
});

I think it is an easy way to solve the problem described here #5 (comment)

Help request: Map implementation

I am trying to implement set serialization, and have checkout out yours, though did not understand it.
How does it work ?
My aim is to have one of my reducers populate and read stateful Map in Redux Store, how would I do that ?

const reducer = (state = {
  statefulMap: new Map()
}, action) => {
switch
    case: {
      let newMap = new Map(state.statefulMap)
      action.newObjects.forEach(newMap.set(`${p.id},${p.search_keyword}`, 
      return Object.
return {
          statefulMap: newMap
        }
    }

Thanks for any thoughts.

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.