Wondering what’s next for npm?Check out our public roadmap! »

    simred

    2.1.2 • Public • Published

    simred NPM version Build Status Tests Coverage

    A simple redux-like application state manager

    • Does not use actions objects, but function calls
    • Supports async actions out of the box
    • Does not require redux-thunk, similar functionality is provided
    • Does not require redux-actions
    • Does not require reduce-reducers to handle relations between parts of state
    • Third parties can listen to state changes through subscribe()
    • Supports middlewares through addMiddleware()
    • Has type definitions for Typescript™

    TODO

    • Allow for "scoped" reducers
    • Code Splitting

    Table of Content

    Installation

    $ npm install --save simred

    Getting Started

    The first example is a simple counter. Same as other state management libraries such as Redux, the state in contained in a single readonly object. This only way to change it is to call "actions". To determine how actions change the state, you write pure reducers.

    import Simred, { withInitialState } from 'simred'
     
    /**
     * This is a reducer,
     * an object with only functions with signatures of:
     * (state, actions, ...args) => state
     * It discribes operation to transform the state
     *
     * The shape of the state is up to you.
     */
    const counter = withInitialState(0)(
      {
        increment: (state) => () => state + 1,
        decrement: (state) => () => state - 1
      }
    )
     
    // Creates a Simred store to hold your data
    const store = Simred.createStore({ counter })
     
    // You can subscribe to any changes to the state to make your
    // app reactive to changes, or store the state to localStorage
    store.subscribe(state => console.log(state))
     
    // The only way to change the state is to call an action
    // you can access actions through the getActions() getter
    const actions = store.actions
     
    actions.counter.increment() // state == { counter: 1 }
    actions.counter.increment() // state == { counter: 2 }
    actions.counter.decrement() // state == { counter: 1 }

    Core concepts

    Simred works in a similar way as Redux, and respects the same three principles:

    Single source of truth The global state is stored in a single object, in a single store.

    State is read-only The only way to change the state is though actions.

    Changes are made with pure functions Actions in the reducers should not mutate the state, but return a new object.


    Basics

    Actions

    Actions are the only way to change the state. They are functions of a reducer (see next section) with the following signature:

    // synchronous actions
    (state: State, actions: Actions) => (...args: any[]) => Partial<State>
     
    // asynchronous actions
    (state: State, actions: Actions) => async (...args: any[]) => Partial<State>

    The state argument is a copy of the part of state the action relates to. The actions argument is a read-only object containing all other actions in the store.

    Actions return an update of state.

    Actions can:

    • be asynchronous, return Promises and call other actions.
    • return a partial state.

    Actions must:

    • return an new object, not mutate the state.

    Important

    Example declaration: const increment = (state, actions) => (n) => state + n

    Example call: actions.counter.increment(3)

    Reducers

    A reducer is a collection of actions to manage a part of state. They allow to organize your code better, splitting logic across separate reusable modules.

    An initial state can be provided to define default values for the part of state the reducer manages. If no initial state is provided, the reducer manages the whole state.

    Simred exposes a dedicated decorator function to create reducers:

     withInitialState(initialState?: any): (actions: any) => Reducer;

    Examples: A reducer managing only a part of state:

    import { withInitialState } from 'simred'
     
    const actions = {
      add: (state, actions) => (item) => [...state, item]
    }
     
    const initialState = []
     
    export default withInitialState(initialState)(actions)

    A reducer managing the whole state:

    import { withInitialState } from 'simred'
     
    const actions = {
      add: (state, actions) => (item) => ({
        list: [...state.list, item],
        counter: state.list.length + 1
      })
    }
     
    export default withInitialState()(actions)

    Combining reducers

    import reducerA from 'reducerA' // Has initialState == []
    import reducerB from 'reducerB' // Has initialState == { value: false }
     
    export const rootReducer = {
      a: reducerA,
      b: reducerB
    }
     
    // storeState == { a: [], b: { value: false } }

    Actions from reducerA will receive storeState.a as state. Actions from reducerB will receive storeState.b as state.

    Actions in reducerB can only interact with storeState.a by calling actions declared in reducerA through the actions argument.

    Example:

    // reducerA.js
     
    // ...
     
    export default withInitialState([])(
      {
        add: (state, _actions) => (item) => [...state, item]
      }
    )
    // reducerB.js
     
    // ...
     
    export default withInitialState({ value: false})(
      {
        setValue: (state, actions) => (value) => {
          // calls "add" from reducerA
          actions.a.add(value)
     
          return { value }
        }
      }
    )

    Store

    Now that we know about actions and reducers, let's bring them all together: that is what the store does.

    • The store holds the application state;
    • Allows access through getState();
    • Allows access to actions (and therefore change the state) through getActions();
    • Register listeners through subscribe();
    • Registers middlewares thtough addMiddleware();

    The store can be created easily once reducers have been combined:

    import Simred from 'simred'
    import { rootReducer } from './reducers'
     
    const store = Simred.createStore(rootReducer)

    An initial state can be specified as second argument. This is useful to hydrate the state with user data matching the Simred state from external sources (server, localStorage, ...)

    import Simred from 'simred'
    import { rootReducer } from './reducers'
     
    const stateFromLocalStorage = JSON.parse(localStorage.getItem['simred_state'])
     
    const store = Simred.createStore(rootReducer, stateFromLocalStorage)

    Calling actions

    import { VisiblityFilters } from './filters'
     
    // Log the initial state
    console.log(store.getState())
     
    // Every time the state changes, log it
    // Note that subscribe() returns a function for unregistering the listener
    store.subscribe(state => console.log(state))
     
    const actions = store.actions
     
    // Dispatch some actions
    actions.todos.add('Learn about actions'))
    actions.todos.add('Learn about reducers'))
    actions.todos.add('Learn about store'))
    actions.todos.toggle(0)
    actions.todos.toggle(1)
    actions.visiblityFilter.set(VisibilityFilters.SHOW_COMPLETED)

    Middlewares

    Middlewares can be used to intercept actions as they are propagated through the store. They can be used for a variety of things, such as logging, storing the state, etc.

    A middleware consist in a function with signature:

    (store: Store) => (next: Function) => (action: string, args: any[]) => void

    Where:

    • store is the store, with .actions, .getState(), etc
    • next is the next middleware in the stack, middleware are executed in the same order they added to the store
    • action is the name of the part of state and of the function (e.g.: "todos.add")
    • args is an array of arguments the action received (e.g.: ["Learn about actions"])

    Middleware can be added in one of two ways:

    • using Simred.createStore(reducers, initialState, middlewares), where the third argument is an array of middlewares
    • using store.addMiddleware(middleware), on an existing store

    Here is an example of a logging middleware:

    // The middleware itself
    const logger = store => next => (action, arts) => {
      console.log({ action, args })
     
      next() // Calls the next middleware on the stack
    })
     
    // add it at store creation time
    const store = Simred.createStore(reducers, {}, [logger])
     
    // OR after the fact
    store.addMiddleware(logger)

    Complete Example: To-do List

    Let's design a store for a simple to-do list manager.

    Todos belongs a single list troughout the application. The have a text and a completed status.

    We want the following functionalities:

    • add a todo to the list of todos;
    • toggle the completed status of a given todo;
    • show either: all todos, completed todos, or remaining todos.

    Designing the state

    We'll use Typescript™ notation for easy reading.

    We identified two types of entities:

    • todos
    • visibility filer

    We'll need two parts of state, and therefore, two reducers:

    // One Todo item
    interface Todo {
      text: string
      completed: boolean
    }
     
    // Visibility Filter constants
    type VisibilityFilter = "SHOW_COMPLETED" | "SHOW_REMAINING" | "SHOW_ALL"
     
    // Shape of the Application State
    interface State {
      todos: Todo[]
      visibilityFilter: VisiblityFilter
    }

    The Todo Reducer

    // todoReducer.js
    import { withInitialState } from 'simred'
     
    /* Initial state for todos: an empty list */
    const INITIAL_STATE = []
     
    export const todoReducer = withInitialState(INITIAL_STATE)({
      /* appends a new todo to the list, completed defaults to false */
      add: (state) => (text) => [...state, { text, completed: false }],
      /* toggles the completed flag of the todo at index */
      toggle: (state) => (index) => {
        return state.map((todo, i) => {
          if (== index) {
            return { ...todo, completed: !todo.completed }
          }
          else {
            return todo
          }
        })
      }
    })

    The Visibility Filter Reducer

    // filters.js
     
    /* Constants for the visibility filter */
    export const VisibilityFilters = {
      SHOW_COMPLETED: "SHOW_COMPLETED",
      SHOW_REMAINING: "SHOW_REMAINING",
      SHOW_ALL: "SHOW_ALL",
    }
    // visibilityFilterReducer.js
    import { withInitialState } from 'simred'
    import { VisibilityFilters } from './filters'
     
    // The application will show all todos by default
    const INITIAL_STATE = VisibilityFilter.SHOW_ALL
     
    export const visibilityFilterReducer = withInitialState(INITIAL_STATE)({
      /* changes the value of the visibilty filter */
      set: () => (filter) => filter
    })

    The Root Reducer

    // reducers.js
    import { todoReducer } from 'todoReducer'
    import { visibilityFilterReducer } from 'visibilityFilterReducer'
     
    /**
     * the todoReducer will manage the part of state 'todos'
     * the visibilityFilterReducer will manage the part of state 'visibilityFilter'
     */
    export const rootReducer = {
      todos: todoReducer,
      visibilityFilter: visibilityFilterReducer
    }

    The Store

    // store.js
    import Simred from 'simred'
    import { rootReducer } from './reducers'
     
    const store = Simred.createStore(rootReducer)
     
    // log all changes to the state
    store.subscribe(state => console.log(state))
     
    export default store

    A test program

    // index.js
    import store from 'store'
     
    const actions = store.actions
     
    actions.todos.add('Learn about actions'))
    actions.todos.add('Learn about reducers'))
    actions.todos.add('Learn about store'))
    actions.todos.toggle(0)
    actions.todos.toggle(1)
    actions.visiblityFilter.set(VisibilityFilters.SHOW_COMPLETED)

    Console:

    { todos: [ { text: "Learn about actions", completed: false } ], visibilityFilter: "SHOW_ALL" }
    { todos: [ { text: "Learn about actions", completed: false }, { text: "Learn about reducers", completed: false } ], visibilityFilter: "SHOW_ALL" }
    { todos: [ { text: "Learn about actions", completed: false }, { text: "Learn about reducers", completed: false }, { text: "Learn about store", completed: false } ], visibilityFilter: "SHOW_ALL" }
    { todos: [ { text: "Learn about actions", completed: true }, { text: "Learn about reducers", completed: false }, { text: "Learn about store", completed: false } ], visibilityFilter: "SHOW_ALL" }
    { todos: [ { text: "Learn about actions", completed: true }, { text: "Learn about reducers", completed: true }, { text: "Learn about store", completed: false } ], visibilityFilter: "SHOW_ALL" }
    { todos: [ { text: "Learn about actions", completed: true }, { text: "Learn about reducers", completed: false }, { text: "Learn about store", completed: false } ], visibilityFilter: "SHOW_COMPLETED" }

    Usage with React

    See the react-simred project to learn how to use Simred with React

    License

    MIT © Gaël PHILIPPE

    Install

    npm i simred

    DownloadsWeekly Downloads

    2

    Version

    2.1.2

    License

    MIT

    Unpacked Size

    59.2 kB

    Total Files

    26

    Last publish

    Collaborators

    • avatar