stately-react
TypeScript icon, indicating that this package has built-in type declarations

0.2.12 • Public • Published

stately-react

[api] [github]
npm

This module contains type-safe components for simplifying React state management.

Usage Guides

Subscribable: like react-redux, but better

Subscribable makes it easy to create components with type-checked data and Store subscriptions using the Render Prop design pattern.

Subscribable is designed to handle any type of subscription. By far the most common use case is subscription to a Redux store, so that is what I will describe here. To see more advanced use cases, e.g. subscribing directly to an RxJS Subject, check out the tests in Subscribable.spec.tsx.

The best way to show how Subscribable can be used is by example.

Standalone <Subscription>

Using a <Subscription> component as a direct injection of state from a Redux store:

// store definition
import { createStoreContext } from 'stately-react'
export const store = createStore(...)
export const { Subscription } = createStoreContext(store)

// elsewhere...
<Subscription>{
  (state, dispatch) =>
    <div>
      {/* render something interesting with state and dispatch */}
    </div>
}</Subscription>

<Subscription> with <Subscriber>s

Typically, you'll use the state of a Redux store or other subscription in many places throughout your application. For that, you'll want to incorporate the use of <Subscriber> descendants of the <Subscription> component:

// store definition
import { createStoreContext } from 'stately-react'
export const store = createStore(...)
export const { Subscription, Subscriber } = createStoreContext(myStore)

// In the root of your component tree:
const MyApp = () => (
  <Subscription>
    <div>
      {/* the rest of your app goes here */}
    </div>
  </Subscription>
)

// Somewhere, a descendant component:
const MyStateful = () => (
  <Subscriber>{
    (state, dispatch) =>
      <div>
        {/* render something interesting with state and dispatch */}
      </div>
  }</Subscriber>
)

The subscriber() decorator

You probably already have components whose props come directly from a subscription to a Redux store. The module react-redux uses the connect() high-order component to inject store state into components via props. The subscriber() decorator is similar to connect(); however, since it was defined in the scope of your Store using createStoreContext(myStore), it is capable of preserving type information, granting you confidence that you've mapped your state to props correctly:

// store definition
import { createStoreContext } from 'stately-react'
const store = createStore(...)
export const { Subscription, subscriber } = createStoreContext(store)

// Component with props from Redux
interface MyComponentProps {
  className: string,
  changeClassName: (newClass: string) => void
}

class MyComponent extends React.Component<MyComponentProps> { ... }

// Apply the decorator
const SubscriberMyComponent = subscriber(

  // Types are checked.
  (state, dispatch) => ({
    className: state.className,
    changeClassName: (newClass: string) => { dispatch(changeClassNameAction(newClass)) }
  })

)(MyComponent)

// elsewhere...
const UsingMyComponent = () => (
  // `className` and `changeClassName` do not need to be provided.
  // They have been injected by `subscriber()`.
  <SubscriberMyComponent />
)

Async Components

<Async> and <CallableAsync> are Controllable components, meaning they can operate either with or without a Redux store backing them. If no Store is used, they will manage their own state internally using React's setState().

In either case, the usage of the <Async> components is the same. To integrate with a Store requires only that they are additionally wrapped with an <AsyncController>. Many use cases do not require a Controller, but some do, such as sharing the asynchronous state with other components, or custom handling of the AsyncActions.

Integrating <Async> with the store will also allow you to see the actions and state mutations as they are dispatched in real-time using the Redux DevTools, which can be useful for debugging.

Declarative <Async> operations

<Async> is "declarative", meaning that the params are passed in as a prop and the operation is performed automatically. It should be used whenever a component needs asynchronously-loaded data to render, such as search results or an entity from a REST service.

This example uses <Async> to wrap a <SearchResults> component, handling the execution and state management of the asynchronous doSearch operation:

import { Async } from 'stately-react'
import { doSearch } from './search'

// type doSearch = (p1: number, p2: string) => Promise<SearchResults>

<Async operation={doSearch} params={[123, 'abc']}>{
  state =>
    <div>{
      state.error ? <ErrorMessage error={state.error} />

        : state.data ? <SearchResults results={state.data} />

        : state.status === 'active' ? <LoadingSpinner />

        : null
    }</div>
}</Async>

Reactive <Async> operations

<CallableAsync> is "reactive", meaning that you can initiate the call programatically, usually in response to a user interaction event.

This example uses <CallableAsync> to invoke save when the button is clicked:

import { CallableAsync } from 'stately-react'
import { save } from './saveEntity'

// type save = (entity: Entity) => Promise

<CallableAsync operation={save}>{
  (state, callSave) =>
    <div>{
      state.error ? <ErrorMessage error={state.error} />

        : state.status === 'complete' ? <SuccessMessage message="Entity saved successfully" />

        : state.status === 'active' ? <LoadingSpinner />

        : null
    }<EntityForm onSubmit={(entity: Entity) => callSave(entity)} />
    </div>
}</CallableAsync>

<Async> operations with Redux

<Async> is Controllable, so it can be integrated with a Redux store by providing the store's state and dispatch to a parent <AsyncController>:

import { AsyncController, Async } from 'stately-react'
import { Subscription } from './store'

<Subscription>{
  (state, dispatch) =>
    <AsyncController state={state} dispatch={dispatch}>

      // Any component nested under the AsyncController
      <Async operation={doSearch} params={[123, 'abc']}>{
        (state) => ...
      }</Async>

    </AsyncController>
}</Subscription>

Controllable components

Controllable components manage their internal state with actions and reducers, allowing you to create components that can be used with or without a Store connection or external state management.

Async components are Controllable.

By default, <Controllable> components create their own internal state and dispatch wrapped around React's setState system. By providing an ancestral <Controller>, the consumer can provide a type-checked overriding state and dispatch that will be used instead. Any actions triggered by the child are passed to the given dispatch, and the component will render using the given state.

Controllable components with Redux

Integrating a Controllable component with a Redux store is a two-step process.
First, the Store must be configured with the component's reducer (and middleware, if present):

import { createStore, applyMiddleware } from 'redux'
import { merge } from 'stately-reducers'
import { createStoreContext } from 'stately-react'

import { controllableReducer, controllableMiddleware } from './MyControllable'
import { myReducer } from './myReducer'

export const store = createStore(
  merge(
    controllableReducer,
    myReducer
  ),
  // middleware is optional, can perform side effects
  applyMiddleware(controllableMiddleware)
)

export const { Subscription, Subscriber, subscriber } = createStoreContext(store)

Second, a <Controller> corresponding to the <Controllable> component must be placed somewhere in the component tree above a <Controllable> component in question. Every Controllable component must export a compatible Controller. For example, <AsyncController> is exported alongside <Async>.

See the <Async> example.

To take control of all <Async> components in your entire app, you might put this code at the root of your application:

import { Subscription } from './store'

<Subscription>{
  (state, dispatch) =>
    <AsyncController state={state} dispatch={dispatch}>

      {/* the rest of your app goes here */}

    </AsyncController>
}</Subscription>

Once you have integrated the <Controller>, you can manage the <Controllable> component's state however you wish. In the previous example, Async actions are now being dispatched through the Store, so you could, for example, create your own reducer to handle the async/.../data actions of an asynchronous call.

Implementing a Controllable component

"Wow! These Controllable components are great! That pattern would work perfectly for my module!"
-you, probably

Implementing a Controllable component is easy, especially if you're familiar with React Context. To start with, you need to define actions and a reducer to manage the state of your component. Using createControllableContext(), you can then create a Controllable/Controller pair with the reducer. <Controllable> components can integrate middleware, as well:

import { createControllableContext } from 'stately-async'

const myReducer = ...

export const { Controller, Controllable } = createControllableContext(myReducer, myMiddleware)

// ... continued below

<Controllable> will provide an internally-managed state and dispatch utilizing the given reducer. Alternately, any parent consumer can use your <Controller> to pass in the state and dispatch. The children of the <Controllable> can use these to render and update the state:


export const MyControllable = props => (
  <Controllable>{

    // either coming from Controllable internally, or somewhere else!
    (state, dispatch) =>
      <div>
        {/* implement your wonderful and amazing component here */ }
      </div>

  }</Controllable>
)

Check out the implementation of Async.tsx for inspiration.

Package Sidebar

Install

npm i stately-react

Weekly Downloads

2

Version

0.2.12

License

MIT

Unpacked Size

77.3 kB

Total Files

26

Last publish

Collaborators

  • hiebj