UI state

UI state is imperative to get right within your app at an early stage. It has a direct impact on a user's experience and influences the architecture of your apps actions, reducers and other business logic.

Needs

The fundamentals of UI state are different to normal state. UI state is shared across the entire chrome of the browser. This means that UI state for a given page should be shared and editable by all components. When adding UI state to a component state should:

  1. Be stored in a reducer, to allow interactivity within decoupled components
  2. Be automatically passed down to any component
  3. Be easy to update in any component, via actions
  4. Have a set of default props for the current view, preferably set in the component
  5. Automatically reset to the default state when navigating away from the current view
  6. Optionally persist state upon navigation

This is a complex set of requirements for transient stated. Using setState within a component doesn't meet many of our needs; it's hard to pass down as props and even harder to manipulate cleanly.

Managing UI state with redux-ui

redux-ui is a higher-order decorator which wraps each component with UI state and actions to change state. It solves all the above needs, and works similarly to block level scoping.

redux-ui fundamentals

In the following example each curly brace pair represents a React component, and each variable represents a piece of UI state:

{
  let display = "SHOW_ALL";
  let filter = '';

  {
    let isEditing = false;
  }
}

We'd like to define a parent view which has display and filter UI state. The child view defines its own isEditing state. Keeping block-scope fashion, the child view can still access and change UI variables from the parent. This makes editing UI state easy from any component.

Here's how we set it up within redux-ui:

// Use redux-ui's @ui decorator to create a new context for saving UI state 
// in this component This is the root component of our app.
@ui({
  // Save all state within the 'contacts' key of the UI reducer
  key: "contacts",
  // Define default state. All state vars must be defined in a UI decorator
  state: {
    display: "SHOW_ALL", // enum, SHOW_ALL/SHOW_STARRED
    filter: '' // The search filter to live search contacts
  }
})
@connect(contactsSelector)
class Contacts extends Component {

  static propTypes = {
    ui: PropTypes.object,
    updateUI: PropTypes.func,
    contacts: PropTypes.object
  }

  updateFilter(evt) {
    this.props.updateUI('filter', evt.target.value);
  }

  // Return a function with a closure referencing the string to change to
  updateDisplay = (filter) => (evt) => {
    this.props.updateUI('display', filter);
  }

  render() {
    const { contacts, ui } = this.props;
    return (
      <div>
        <input type='text' value={ ui.filter } onChange={ ::this.updateFilter } />
        <a href='#' onClick={ ::this.updateDisplay('SHOW_ALL') }>All</a>
        <a href='#' onClick={ ::this.updateDisplay('SHOW_STARRED') }>Starred</a>
        { Object.keys(contacts).map(c => <ContactItem contact={ contacts[c] } />) }
      </div>
    );
  }
}

// This is our child component. It is given **no** key so each component will
// generate a key at instantiation. As there are many of these components each
// will have its own context; if we gave this a key each of these components
// would share UI state.
@ui({
  state: {
    isEditing: false
  }
})
class ContactItem extends Component {

  static propTypes = {
    ui: PropTypes.object,
    updateUI: PropTypes.func,
    contact: PropTypes.object.isRequired
  }

  render() {
    const { contact, ui } = this.props;
    return (
      <div>
        <p>{ contact.name }</p>
        {/* Note that this is also given all parents UI state */}
        {/* We could even call `updateUI('filter', 'something') to update it */}
        { (ui.filter !== '') && <small>Matches { ui.filter }</small> }
      </div>
    );
  }
}

In this small example we've achieved a lot via the decorator:

  • Our views define UI state variables with defaults in the component
  • We automatically receive UI state as props
  • We're given an updateUI method to change UI state
  • All child components inherit their parent's UI context
  • Child components are still given their own UI context for independence
  • The UI state for each component will be deleted on unmount
  • We've written no reducers or actions ourselves

This leads to incredibly powerful, reusable componnets which still manipulate UI state such as drop-in pagination.

See redux-ui for more information.