UI state management for Redux applications
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
examples/counterTypeScript
src
.gitignore
.npmignore
.travis.yml
LICENCE.md
jest.json
package-lock.json
package.json
readme.md
rollup.config.js
tsconfig.json
tslint.json

readme.md

Redux UI State

version licence build gzip size size downloads

UI state management for Redux applications.

Makes storing UI state in the Redux store and sharing it between components simple, safe and transparent.

WARNING: This should only be used for storing small amounts of UI data. Almost all of your application's data should be managed using standard reducers.

Installation

npm install redux-ui-state

Redux UI State is written in TypeScript, so the typings are automatically included and always up to date 🎉

Getting started

NOTE: This is a simple implementation. For more complex implementations (e.g. custom Redux state shape and a dynamic config id), see the example folder.

1. Create the reducer for your app:

Your root reducer should look this:

// rootReducer.js

import { combineReducers } from 'redux';
import { createReducer, DEFAULT_BRANCH_NAME } from 'redux-ui-state';

const initialState = {
  counter: {
    index: 0
  },
}

export default combineReducers({
  ...
  [DEFAULT_BRANCH_NAME]: createReducer(initialState),
  ...
});

2. Use the data in your UI:

Render Prop implementation

Use the setupCreateUIState higher order component to inject uiState (to be used in place of this.state) and setUIState (to be used in place of this.setState) into your render prop.

//////////////////////////////////////////////////
// UIState.js

// This setupCcreateUIState higher order function allows a custom selector for the ui branch of the
// redux store to be passed in if the ui branch is not named 'ui' and at the root. The call to 
// setupConnectUIState should only be done once in your application and the connection function
// should be exported.
const createUIState = setupConnectUIState();

//////////////////////////////////////////////////
// CounterUIState.js

// Either create a render prop component that maps the data into nicely structured props (preferred)
export const CounterUIStateMapped = createUIState(
  'counter',
  ({ uiState, setUIState }) => ({
    index: uiState.index,
    increment: () => setUIState({ index: uiState.index + 1 }),
    decrement: () => setUIState({ index: uiState.index - 1 })
  })
);

// Or create a render prop component that recieves the raw props
export const CounterUIStateUnmapped = createUIState<UIState>('counter');

//////////////////////////////////////////////////
// App.js
import React from 'react';
import Counter from './Counter';

const App = () => (
  <div>

    <UIStateMapped>
      {({ index, increment, decrement }) => (
        <div>
          <div>Value: {index}</div>
          <div>
            <button onClick={decrement}>-</button>
            <button onClick={increment}>+</button>
          </div>
        </div>
      )}
    </UIStateMapped> 
     
    <UIStateUnmapped>
      {({ uiState: { index }, setUIState }) => (
        <div>
          <div>Value: {index}</div>
          <div>
            <button onClick={() => setUIState({ index: index - 1})}>-</button>
            <button onClick={() => setUIState({ index: index + 1})}>+</button> 
          </div>
        </div>
      )}
    </UIStateUnmapped>
    
  </div>
);

Higner order component implementation

Use the setupConnectUIState higher order component to inject uiState (to be used in place of this.state) and setUIState (to be used in place of this.setState) into your component.

//////////////////////////////////////////////////
// UIState.js

// This setupConnectUIState higher order function allows a custom selector for the ui branch of the
// redux store to be passed in if the ui branch is not named 'ui' and at the root. The call to 
// setupConnectUIState should only be done once in your application and the connection function
// should be exported.
const connectUIState = setupConnectUIState();

//////////////////////////////////////////////////
// Counter.js
import React from 'react';
import { defaultConnectUIState as connectUIState } from 'redux-ui-state';

const Counter = ({ indexMessage, increment, decrement }) => (
  <div>
    <div>
      {indexMessage}
    </div>
    <div>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  </div>
);

export default connectUIState(
  'counter',
  ({ index }, { setUIState }, { prefix }) = ({
    indexMessage: `${prefix}${index}`,
    decrement: () => setUIState({ index: index - 1 }),
    increment: () => setUIState({ index: index + 1 }),
  })
)(Counter);

//////////////////////////////////////////////////
// App.js
import React from 'react';
import Counter from './Counter';

const App = () => (
  <div>
    <Counter prefix="Value: " initialIndex={0} />
  </div>
);

Rationale

Shorter practical answer 🔨

Sharing UI state between components usually means either:

  • Adding state to a parent component that shouldn't really be concerned with it and passing it through view layers that also shouldn't be concerned with it.
  • Creating a custom UI state reducer with specific actions for each atom of state that will change.

Redux UI State replaces these overly complex, error prone solutions with a reducer and a higher order component that provides clean access to UI state held in the Redux store.

Longer theoretical answer 🤓

Single state atom

Redux is a predictable, easy to undertand state container primarly because it has a single state object. This allows developers to inspect the entire state of an application at any given time and create powerful dev tools that enable magical things like time travel debugging. Keeping everything in one place is a large part of what drove Redux to win out over the traditional multi store Flux architecture yet most React applications let multiple data stores in through the back door by using this.setState in components.

As soon as this happens the single state atom principle breaks down, the entire application state cannot be inspected and full state serialization / time travel debugging becomes impossible.

Sharing state with siblings

Sibling components sometimes need to share their UI state. This can be achieved by moving that state up into the parent component, but this is often not something the parent should be concerned with, which means increased complexity, confusing APIs, less reusability and the system as a whole being more prone to errors.

Sharing state with children

In situations where it does make sense for a parent to hold a piece of UI state, that data may be needed several layers down in the view hierarchy, meaning intermediate components, unconcerned with the data in question, need to include it in their APIs, again creating unnecessary complexity, which results in confusing APIs and a greater likelihood of errors cropping up.

A solution

Redux UI State replaces these overly complex, error prone solutions with a higher order component that provides clean access to UI state held in the Redux store.

TODO / Roadmap

  • Add Github pages page
  • Add documentation for advanced implementations
  • Add plain JavaScript examples
  • Add flow type definitions