npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

redux-container-state

v0.4.0

Published

Local container state for Redux based on the Elm Architecture

Downloads

15

Readme

redux-container-state

This project is an attempt to integrate local container state into a Redux store, which is global by nature.

Influences

This project evolves the ideas redux-elm, but avoids opinions about specific implementations of Side Effects and tries to be more in line with the Redux approach of reducers.

Because this project is influenced by redux-elm, which is in its term highly influenced by The Elm Architecture, most concepts will be familiar to both projects.

What is this project trying to solve?

This project tries to solve the same problem as the redux-elm project. Basically, it comes down to this:

In Redux, state is considered global. That makes it hard to create isolated and reusable container components, which require their own local state. This projects tries to abstract away the complexity to handle this problem.

The Gist

redux-container-state is about reusable Containers (which are basically React components that control pieces of an application). These Containers typically require some local state. To solve this within redux, you have to find solutions to link actions and components to dedicated locations within the state graph.

With redux-container-state, it should become a lot more easy to solve this, because your reusable containers do not have to care about the above problem anymore.

To create containers that benefit from this approach, your container should at least consist of 2 pieces:

  • View: This is your typical React implementation, wrapped within a higher order component that handles isolation.
  • Updater: This is your typical Redux reducer, wrapped within a higher order function that handles isolation.

In the Elm architecture, the typical example is about a counter. So, lets get started with the same example.

Counter

Counter updater
import { updater } from 'redux-container-state'

const initialModel = 0

// The updater(...) function handles isolation of the Counter reducer.
export default updater((model = initialModel, action) => {
  switch (action.type) {

    case 'Increment':
      return model + 1

    case 'Decrement':
      return model - 1
      
    default:
      return model
  }
})
Counter view
import React from 'react'
import { view } from 'redux-container-state'

export default view(({ model, localDispatch }) => (
  <div>
    <button onClick={() => localDispatch({ type: 'Decrement' })}>-</button>
    <div>{model}</div>
    <button onClick={() => localDispatch({ type: 'Increment' })}>+</button>
  </div>
))

Composition

The Counter sample is the most simple example, but it should give you an idea of the way of working.

Pair of counters

To up the ante, let us create a parent container that holds two Counters (the pair-of-counters use case): a topCounter and a bottomCounter.

You can re-use the Counter updater and view from the example above. After all, this is the whole idea behind this project: being able to reuse containers.

Little side note: you actually need to change one detail in the Counter example above: the parent view should know the initial state of its child containers. This requires you to just export the default initalModel of the Counter updater:

export default const initialModel = 0
Parent view
import React from 'react'
import { forwardTo, view } from 'redux-container-state'

import Counter from '../counter/view'

export default view(({ model, localDispatch }) => (
  <div>
    <Counter model={model.topCounter} localDispatch={forwardTo(localDispatch, 'TopCounter')} />
    <Counter model={model.bottomCounter} localDispatch={forwardTo(localDispatch, 'BottomCounter')} />
    <button onClick={() => localDispatch({ type: 'Reset' })}>RESET</button>
  </div>
))

The above sample actually explains the internal working of the composition mechanism: the parent container prepares a new localDispatch method for its child containers. This new localDispatch method is capable of composing an hierarchical action.

For instance: forwardTo(localDispatch, 'TopCounter') creates a new localDispatch method that will wrap dispatches of the child container into the TopCounter context. This context can then be used within the parent updater to inspect the targetted child container.

Parent updater
import { updater } from 'redux-container-state'
import counterUpdater, { initialModel as counterInitialModel } from '../counter/updater'

const initialModel = {
  topCounter: counterInitialModel,
  bottomCounter: counterInitialModel
}

export default updater((model = initialModel, action) => {
  switch (action.type) {

    case 'Reset':
      return initialModel

    case 'TopCounter': 
      return {
        ...model,
        topCounter: counterUpdater(model.topCounter, action)
      }

    case 'BottomCounter': 
      return {
        ...model,
        bottomCounter: counterUpdater(model.bottomCounter, action)
      }
      
    default:
      return model
  }
})

The parent updator is aware of its child updaters. The library takes care of unwrapping the parent's action into a child action, so you just pass the action to the child updater.

Dynamic composition

In a lot of cases, a hard-coded set of child containers is sufficient. However, there is a huge use-case for a dynamic set of child containers.

Dynamic list of counters

Parent view
import React from 'react'
import { forwardTo, view } from 'redux-container-state'

import Counter from '../counter/view'

const viewCounter = (localDispatch, model, index) =>
  <Counter key={index} localDispatch={ forwardTo(localDispatch, 'Counter', index) } model={ model } />

export default view(({ model, localDispatch }) => (
  <div>
    <button onClick={ () => localDispatch({ type: 'Remove' }) }>Remove</button>
    <button onClick={ () => localDispatch({ type: 'Insert' }) }>Add</button>
    {model.map((counterModel, index) => viewCounter(localDispatch, counterModel, index))}
  </div>
))

Because there are an unknown amount of child containers, the parent view is not capable of forwarding the localDispatch method in a predictable way (without trickery code, that is).

That is why the forwardTo method can take an additional parameter, which parameterizes the action type that is being forwarded. This parameter can then be used within the parent's updater.

Note: this parameter can be of any type, but it should be serializable to a string (e.g. integers, strings, floats, ...).

Parent updater
import { updater } from 'redux-container-state'
import counterUpdater, { initialModel as counterInitialModel } from '../counter/updater'

export default updater((model = [], action) => {

    switch (action.type) {

        case 'Insert': 
            return [
                ...model,
                counterInitialModel
            ]

        case 'Remove':
            if (model.length > 0) {
                const counters = [ ...model ]
                counters.pop()
                return counters
            }
            return model

        case 'Counter':
            return model.map((counterModel, index) => {
                if (index === action.typeParam) {
                    return counterUpdater(counterModel, action)
                }
                return counterModel
            })

        default:
            return model
    }
})

The updater can use the typeParam to check the targetted child container (as set in the forwardTo method). Because parameterization is mainly used within the context of arrays (and thus the index in the array will be the parameter), the framework deserializes numbers back to valid JavaScript types (which takes away the burder to have to parse the parameter to a number yourself.)

View enhancers

Local middleware

Note: This is highly experimental and has not validated against multiple use-case. However, local thunk middleware is up and running. Yay!

In order for your reusable container to be truly isolated, you probably need some middleware that only applies to your container only.

import React from 'react'
import { compose } from 'redux'
import { view, applyLocalMiddleware } from 'redux-container-state'
import localThunk from 'redux-container-state-thunk'


const increment = () => {
  return {
    type: 'INCREMENT_COUNTER'
  }
}

const incrementAsync = () => {
  return (localDispatch, getState) => {
    setTimeout(() => {
      localDispatch(increment());
    }, 1000)
  }
}

const counterUpdater = updater((model = 0, action) => {
  switch (action.type) {
    case 'INCREMENT_COUNTER': 
      return model + 1
    default:
      return model
  }
})

const viewWithMiddleware = compose(applyLocalMiddleware(localThunk))(view)

// Pass the middlewares you need to the view method.
export default viewWithMiddleware(({model, localDispatch}) => (
  <div>
    <button onClick={ () => localDispatch(incrementAsync()) }>Start counter</button>
    Current count: { model }
  </div>
))

Global state

In some cases, you will want to get access to the global state of Redux within your view(). For that use-case, take a look at the global-state enhancer at redux-container-state-globalstate.

Side Effects with redux-saga

If you wish to incoporate redux-saga into your local containers, you can have redux-container-state-globalsaga. This extension enables Sagas that have access to actions and state from both global (redux store) and local (container) sources.

If you need Sagas that work on containers only (so only local actions and state), there is another option as well: redux-container-state-saga

Some remarks

Actions are dispatched globally

Worth noting is that all actions that originate from a view() are dispatched globally. This might not be obvious at first sight, but if you consider that Redux is the backing state store for this framework, it makes a lot more sense.

This also means that you can register any middleware and/or store enhancer in Redux to handle some concerns from your child containers (but be careful, in some cases this is exactly what you don't want).

When composed actions are send to Redux, they will follow a predictable format. Suppose you have a container that holds a dynamic list of counters, the type of the action in global scope will look like Counter[4]->Increment. Or deeply nested, it could look like Child[1]->GrandChild[2]->TopCounter->Increment.

Inspecting the global type in an updater

In some cases, you will want to inspect the globally dispatched action. To get a hold of this within an updater(reducer), you can inspect the action.globalType property.

Examples

Installation & Usage

You can install redux-container-state via npm.

npm install redux-container-state --save