StoreApi

Defines an api around a Zedux store or other observable. Used to create component-bound stores (stores that live and die with a React component).

Definition

class StoreApi<TState> {
  _bindControls(
    StoreApiClass: StoreApiConstructor<TState>
  ): StoreApi<TState>
}

interface StoreApi<TState> {
  store: Observable<TState>
}

See the StoreApiConstructor and Observable types.

The gist

To create a component-bound store simply create a class that extends StoreApi. This class must have a visible store instance property whose value is an Observable.

The class may also take static actors and selectors properties. See the StoreApiConstructor type for more details.

This class constructor may be passed directly to createContext(). React Zedux will instantiate it every time the resulting Context's <Provider> (or <Injector>) is mounted. React Zedux will also call storeApi._bindControls() to bind any actors/selectors to the store and merge those and all properties of the store into the api instance itself.

React Zedux will throw an error if there are any duplicate keys between the actors map, the selectors map, and the store's own properties. This mitigates the problems associated with this "mixin" sort of behavior.

Examples

A simple component-bound store.

import React from 'react'
import { StoreApi, createContext } from 'react-zedux'
import { createStore } from 'zedux'

class CounterApi extends StoreApi {

  /*
    The `store` property is required. React Zedux will throw
    an error if it isn't a valid observable.

    Here we're creating a new store every time the CounterApi is
    instantiated. This will create a component-bound store. Note
    that we could set it to a global store if we just want to
    take advantage of the other features offered by StoreApis.
  */
  store = createStore().hydrate(0)
}

/*
  We pass the class constructor directly to `createContext()`.
  React Zedux will instantiate it every time a <Provider>
  is mounted.
*/
const CounterContext = createContext(CounterApi)

// Some components. Part of a complete example.
const Counter = () => (
  <CounterContext.Injector>
    {({ state }) => state}
  </CounterContext.Injector>
)

// Now every time we mount a <Counter>, React Zedux creates
// a new counter store.
const App = () => (
  <>
    <Counter />
    <Counter />
  </>
)

The StoreApi is useful for defining a store's api (hence the name...). The "api" is the interface consumers use to interact with the store. The StoreApi can bind actors and selectors to the store and provide hooks and other utilities for accessing and modifying the store's data.

import React from 'react'
import { StoreApi, createContext } from 'react-zedux'
import { createStore, select } from 'zedux'

class TodosApi extends StoreApi {

  /*
    A StoreApi can take static `actors` and `selectors` properties.
    These will be bound to the store and merged into the TodosApi
    instance.
  */
  static actors = {

    // A simple inducer factory
    addTodo: text => state => [
      ...state,
      { text, isComplete: false }
    ]
  }

  static selectors = {

    // A simple, memoized selector
    selectIncompleteTodos: select(
      state => state.filter(todo => todo.isComplete)
    )
  }

  store = createStore().hydrate([])
}

const TodosContext = createContext(TodosApi)

// Now when we mount a TodosContext.Provider, React Zedux will bind
// these actors and selectors to the store and merge them into the
// api instance.
const Todos = () => (
  <TodosContext.Injector>
    {({ addTodo, selectIncompleteTodos }) => {
      addTodo('be awesome')

      selectIncompleteTodos()
      // [ { text: 'be awesome' isComplete: false } ]
    }}
  </TodosContext.Injector>
)

The actors and selectors can contain nested namespaces. React Zedux will preserve the nesting. This can be useful for creating hooks (such as actors that perform a check before proceeding with a dispatch):

import React from 'react'
import { StoreApi, createContext } from 'react-zedux'

class TodosApi extends StoreApi {
  static actors = {
    wrappedActors: {
      addTodo: text => state => [
        ...state,
        { text, isComplete: false }
      ]
    }
  }

  store = createStore().hydrate([])

  addTodo(text) {
    const todos = this.store.getState()

    if (todos.some(todo => todo.text === text)) {
      return todos // a todo already exists with that name
    }

    // We're good; proceed with the dispatch
    return this.wrappedActors.addTodo(text)
  }
}

const Todos = () => (
  <TodosContext.Injector>
    {({ addTodo }) => {
      addTodo('nest a selector now')
    }}
  </TodosContext.Injector>
)

Testing

Just be sure to call storeApi._bindControls() manually:

import { StoreApi } from 'react-zedux'
import { createStore } from 'zedux'

class TodosApi extends StoreApi {
  static actors = {
    addTodo: text => state => [
      ...state,
      { text, isComplete: false }
    ]
  }

  store = createStore().hydrate([])
}

const todosApi = new TodosApi()
  ._bindControls() // yes, it can be chained

todosApi.addTodo('a')

expect(todosApi.getState()).toBe([
  { text: 'a', isComplete: false }
])

Method Api

_bindControls()

Definition

() => StoreApi<TState>

Returns the StoreApi instance for chaining.

Explanation

Given a StoreApi instance like so:

import { StoreApi } from 'react-zedux'
import { createStore } from 'zedux'

class CounterApi extends StoreApi {
  store = createStore().hydrate(0)

  static actors = {
    increment: () => state => state + 1
  }

  static selectors = {
    selectNumTimesTen: state => state * 10
  }
}

const counterApi = new CounterApi()

calling

counterApi._bindControls()

Will:

  • Merge all properties of counterApi.store into counterApi. Will throw an error if any of those properties already exist on counterApi.

  • Bind the increment actor to the store.

  • Stick the bound increment actor on counterApi. Will throw an error if increment already exists on counterApi.

  • Bind the selectNumTimesTen selector to the store.

  • Stick the bound selectNumTimesTen selector on counterApi. Will throw an error if selectNumTimesTen already exists on counterApi.

Notes

You don't have to use the auto-binding feature of StoreApis. The following two examples are equivalent:

import { StoreApi } from 'react-zedux'
import { createStore } from 'zedux'

class CounterApi extends StoreApi {
  store = createStore().hydrate(0)

  increment() {
    this.store.dispatch(state => state + 1) // or this.dispatch(...)
  }
}

and:

import { StoreApi } from 'react-zedux'
import { createStore } from 'zedux'

class CounterApi extends StoreApi {
  store = createStore().hydrate(0)

  static actors = {
    increment: () => state => state + 1
  }
}

results matching ""

    No results matching ""