Skip to content

lsm/alfa

Repository files navigation

Alfa

Effortless React State Management.

CI status License MIT NPM version Dependencies Coverage Status Code Climate

Alfa - Effortless React State Management

Why Alfa?

React is an excellent library for creating interactive and stateful views. However, things become unclear when you need to share & change data across components.

Alfa offers an intuitive and straightforward way to manage React state. It completely decouples the complex relationships between components and lets you focus on making components that work anywhere.

Its simple design allows you to adopt it in a matter of minutes while at the same time provides your essential tools to keep your application code easy to change and understand. Here is a list of things why it is the perfect fit for your next React app:

  • Easy – Only 4 functions/APIs to learn.
  • Fast – Alfa wraps your components with a thin layer. It introduces little to no performance impacts.
  • Small – ~190LOC & 3KB minified + gzipped.
  • Async – Alfa supports asynchronous operations natively without additional packages.
  • Explicit – Alfa lets you know what a component requires (input) and what it changes (output).
  • Transparent – You can use and unit test your components as they are without Alfa. Users of your lib/component could but don't have to use Alfa at all.
  • React Native – Supports React Native out of the box.
  • Server Render – Supports isomorphic apps out of the box.
  • Production Ready – 100% test coverage and being used in productions.

Links

Quick Guide

Installation

Use npm to add it to your package.json.

npm install --save alfa

Alternatively, use yarn if you prefer:

yarn add alfa

Getting Data for Components

Alfa converts your regular React component into a dependency injected component by injecting application data from a key/value store. Let Alfa handle the data if you use it in different components:

// hello.js
import React from 'react'
import { subscribe } from 'alfa'

// A stateless functional component.
function HelloMessage(props) {
  // Data is injected as the property of props.
  return <div>Hello ${props.name}!</div>
}

export default subscribe(HelloMessage, ['name'])

subscribe makes a new component which gets the variable name and feeds it into the HelloMessage as props.name on (re-)rendering.

Now let's see how to use the above component in our app:

// index.js
import React from 'react'
import { render } from 'react-dom'
import { provide, subscribe } from 'alfa'

import HelloMessage from './hello.js'

// Define the root app which renders HelloMessage as a child.
const App = () => (
  <div>
    <HelloMessage />
  </div>
)

// Create the Root component by wrapping the App component with initial data
// using `provide(Component, data)`.
const Root = provide(App, { name: 'Motoko' })

// Render it!
render(<Root />, document.getElementById('root'))

You don't need to pass the variable name to HelloMessage as Alfa gets it from the store and passes it to HelloMessage component automatically. It allows us to quickly move the component around without worrying about how to get the data it depends on.

Changing Data

The simplest way to modify the data of the Alfa store is to inject the built-in set function to the component.

// change.js
import { subscribe } from 'alfa'
import React, { Component } from 'react'

// A stateful class component.
class ChangeName extends Component {
  handleChange = event => {
    // Calling `set('mykey', 'my value')` will change the data `mykey`
    // in store to value `my value`.
    this.props.set('name', event.target.value)
  }

  handleSubmit = event => {
    event.preventDefault()
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            value={this.props.name}
            onChange={this.handleChange}
          />
        </label>
      </form>
    )
  }
}

export default subscribe(ChangeName, ['set', 'name'], ['name'])

As mentioned earlier, Alfa makes things explicit. So we need to define the output (the 3rd argument when calling the subscribe function) of the component explicitly if we want to change a value of a key in the data store. Otherwise, Alfa complains that we are trying to use set without defining the correct output.

Now add the ChangeName component to App and your index.js ends up like this:

// index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider, subscribe } from 'alfa'

import HelloMessage from './hello.js'

const App = () => (
  <div>
    <HelloMessage />
    <ChangeName />
  </div>
)

// Alternatively, you can use Provider - the declarative interface of the
// provide function.
render(
  <Provider data={{ name: 'Motoko' }}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Now each time you make a change in the input box, React will re-render both HelloMessage and ChangeName components to reflect the change.

You can find the finished version of the above example in the folder examples/hello.

Please check the documentation for API and advanced usages.

Advanced Usage

The hello world example we discussed in the Quick Guide introduced 3 functions set, provide, and subscribe. In this chapter, we cover more advanced usages which help you writing more performant code and organizing them better.

Inject instead of subscribing

Alfa has another function called inject and here's how you can use it:

Import React from 'react'
import { inject } from 'alfa'

class SomeComponent extends React.Component {
  render() {
    return (<h1>this.props.title</h1>)
  }
}

export default inject(SomeComponent, ['title'])

As you may see inject has the same interface as subscribe. The only difference is changing the value of the injected key (title in this case) does not trigger the rerender of the component. Which means it's the perfect choice for static components which won't change after the initial render or dummy components which might be changed only through their parents. Using inject in this case makes your code more performant and use less memory since it does not have to monitor changes of its dependencies like the subscribe does.

Action

What is an Action Function?

In the Quick Guide we learned we could use set function to make changes to the store in a component. However, it is common that you want to reuse the same piece of code which uses set to change some keys in the store. Then action is the function to help you create reusable functions that mutate the store which can be used across components. For a large code base, it also helps you to separate the business logic from you representation code and let you understand what component is doing what. (e.g., if a component needs an action called addTodo then it probably can add a new todo). An action function is a function that give you the ability to read from and write to the store.

How to Define an Action?

To define an action function you need to import the action function from alfa first, then call it with your original function and the input/output desired.

import { action } from 'alfa'

let actionFunction = action(someFunction, inputs, outputs)

Here is an example from the TodoMVC example:

import { action } from 'alfa'

// Action functions only take one argument which must be an object.
function addToDoAction({ text, todos }) {
  var resultTodos

  if (!text) {
    // No `text` then we change nothing. The new todos will be the old one.
    // This will not trigger rerender of the components which subscribe to the
    // `todos` key.

    resultTodos = todos
  } else {
    // We have `text` then construct a new todo item using it and make a new
    // todos array out of the old one. This causes rerender if you return it
    // (see the return statement at the end of this function).

    resultTodos = todos.concat([
      {
        id: Math.random(),
        text: text,
        completed: false
      }
    ])
  }

  // Now replace the new todos with the old one and return it.
  // Return of action function must be an object.

  return {
    todos: resultTodos
  }
}

// `action` takes three arguments: the action function, inputs and outputs.
// It returns the `addToDoAction` function with a new property `alfaAction`
// attached. The `alfaAction` function is a curry function which takes a `store`
// instance and returns a `dependency-injected` version of the `addToDoAction`
// function.

export const addToDo = action(addToDoAction, ['text', 'todos'], 'todos')

Like the design of inject/subscribe, action make it explicit about what are the input and output of the action function. Alfa enforces the output part and throws an error if you return something that is not predefined in the output:

action(
  function({ input1 }) {
    return {
      // This is a legit output.
      output1: 'some result',

      // This causes alfa to throw an error.
      randomOutput: 'some value'
    }
  },
  'input1',
  'output1'
)

Asynchronous Action

For asynchronous operations we can use set in the action function to change the store just like what we have done in the Quick Guide:

function deleteToDoAction({ set, todos, todoID }) {
  var resultTodos = todos.filter(function(todo) {
    return todoID !== todo.id
  })

  // Simulate an async operation.
  setTimeout(function() {
    set({ todos: resultTodos })
    // You can call `set` as many times as you want.
    set('todosCount', todos.length)
  }, 100)
}

export const deleteTodo = action(
  deleteToDoAction,
  ['todoID', 'todos'],
  ['todos', 'todosCount']
)

Since the purpose of the action functions is making changes to the store, so the set function is pre-injected - you don't have to define it as an input of the action function.

How to Use Action in Components?

To use the actions we first need to set them in the store. Alfa then binds the function with the instance of the store, so you don't have to worry about which store it works with when you call the action functions.

import React from 'react'
import { Provider } from 'alfa'

import TodoApp from './App'
import * as todoActions from './todo-actions'

/**
 * Initial state of applicaiton.
 */
const data = {
  // Actions must be first level keys.
  addToDo: todoActions.addToDo,

  deleteToDo: todoActions.deleteToDo

  // Yeah, we can simply use the spread syntax:
  ...todoActions
}

/**
 * Render our application.
 */
render(
  <Provider data={data}>
    <TodoApp />
  </Provider>,
  document.getElementById('root')
)

Then in the component, we can get it as a normal dependency:

// ...

class TodoItem extends Component {
  // ...

  onDeleteClick(todoID) {
    // The deleteTodo action also requires `todos` as input, but we don't have to
    // provide it here as alfa gets it from the store automatically.
    // Note, the parameters fed into the action here have higher priority
    // than the one injected from the store. It allows you to override the
    // input of the action when necessary.

    this.props.deleteTodo({ todoID })
  }

  // ...
}

// ...

export default inject(TodoItem, ['deleteTodo'])

Dynamic Keys

What is Dynamic Keys for?

To make changes to the store, alfa requires you to define the names of the outputs for both injected/subscribed components or action functions. What if the key you want to change is a dynamic value? For example, a unique id for a chat room? You can not predefine the output key in this case because it is data not code. Luckily, alfa supports this use case and it's called Dynamic Keys.

Another use case is when you want to load more dependencies based on the props of the component at initialization stage.

To use it you need to define a function as the static property keys for your component.

How to Define Dynamic Keys?

Let's see how it works with a concrete example. Assume you have a chat app which supports multiple chat rooms and threading in a certain room. To control the open or the close of the threads you need to keep track of which thread is currently active in which chat room. We can use the combination of roomID/activeThreadID as the key to store the state of the active thread. For example, { alfadev/activeThreadID: 'thread23' } means in chat room alfadev the current active thread id is thread23.

// ...

class ChatRoom extends Component {
  // ...

  static keys = props => {
    return {
      chatRoom: props.roomID,
      // Each room can only have one active thread. If you room id is `alfadev`
      // then the key for storing active thread id of this room is
      // `alfadev/activeThreadID`
      activeThreadID: props.roomID + '/activeThreadID'
    }
  }

  // ...
}

// For functional component, add the `keys` property to the function itself.

function ChatRoom(props) {
  // ...
}

ChatRoom.keys = props => {
  return {
    chatRoom: props.roomID,
    activeThreadID: props.roomID + '/activeThreadID'
  }
}

How to Use Dynamic Keys in Components?

The injected props in the component has static names (e.g., chatRoom and activeThreadID). You can get and set them as normal dependencies:

// ...
let chatRoom = this.props.chatRoom
// ...
this.props.set('activeThreadID', 'thread56')
// ...

It helps you write clean and abstract code. Then in our example, a function to toggle the show/hide of the thread looks like this:

// ...

class ChatRoom extends Component {
  // ...

  handleThreadToggle = () => {
    const { thread, activeThreadID } = this.props

    if (activeThreadID === thread.id) {
      this.props.set('activeThreadID', null)
    } else {
      this.props.set('activeThreadID', thread.id)
    }
  }

  // ...
}

When you call this.props.set('activeThreadID', null) Alfa maps the dynamic key back to the real key we defined in the return object of the keys function. In this case the set function call changes the value of the key alfadev/activeThreadID to null or the id of the thread.

Now defining the output of the ChatRoom component becomes more intuitive:

export default subscribe(
  ChatRoom,
  ['set', 'activeThreadID'],
  ['activeThreadID']
)

You can find a full example for the usage of the dynamic keys in injection.test.js test case Should set dynamic key correctly.

Multiple Stores

Sometimes you may want to have multiple stores for different sections of your application. It is easy to achieve with alfa, all you need to do is put components under separate Providers:

function App() {
  return (
    <div>
      <Provider data={data1}>
        <SubApp1 />
      </Provider>
      <Provider data={data2}>
        <SubApp2 />
      </Provider>
    </div>
  )
}

render(<App />, document.getElementById('root'))

Now you won't have any conflicts even you have the same key in both SubApp1 and SubApp2 since they are under different Providers and stores.

API

provide(component, data)

Provide a store to the wrapped component and its children components

  • Parameters

    • component - required Function|Class: Wrapped function or class (React component).
    • data - optional Object|Store: Initial data object or instance of alfa.Store.
  • Return

    Function - it returns a function which you can use it as a regular React component.

Provider

The declarative interface of provide function. It wraps a component and its children with an Alfa store.

// `data` is optional as it is in `provide`.
<Provider data={Object | Store}>
  // Only one child is allowed.
  <SomeComponent />
</Provider>

inject(component, input, output)

Inject data to the component as props based on the keys provided from input.

  • Parameters

    • component - required Function|Class: Wrapped function or class (React component).
    • input - optional Array|String: The keys of input dependencies. Could be a single string or array of strings.
    • output - optional Array|String: The keys of output dependencies. Could be a single string or array of strings.
  • Return

    • Function - it returns a function which we can use it as a regular React component.

subscribe(component, input, output)

Subscribe data to the component as props based on the keys provided from input. Any changes made to the keys found in input triggers rerender of the component.

  • Parameters

    • component - required Function|Class: Wrapped function or class (React component).
    • input - optional Array|String: The keys of input dependencies. Could be a single string or array of strings.
    • output - optional Array|String: The keys of output dependencies. Could be a single string or array of strings.
  • Return

    • Function - it returns a function which you can use it as a standard React component.

action(actionFunction, input, output)

Attach a curry function to the actionFunction which you can use it as dependency-injected version of actionFunction in components under a Provider.

  • Parameters

    • actionFunction - required Function: The action function that makes changes to the store.
    • input - optional Array|String: The keys of input dependencies. Could be a single string or array of strings.
    • output - optional Array|String: The keys of output dependencies. Could be a single string or array of strings.
  • Return

    • Function - it returns the original actionFunction with a curry function attached as property alfaAction.

keys(props)

We can define function keys as a static property of a component or function to support dynamic names of injected/subscribed props. For subscribed component, Alfa calls it at initialization stage (constructor). For injected component, Alfa calls it every time before it renders. For either case, Alfa always calls it before rendering the wrapped component. Then we have the chance to get more dependencies based on the returns of the keys function and merge them with the props of the component. Dynamic keys have higher priority than the original properties in props. Which means a dynamic key overrides the value of the property if they are the same.

  static keys(props) => {
    return {
      userSettings: props.userID + '/settings',
    }
    // Or return an array.
    return ['settings']
  }
  • Parameters

    • props - Object: the original props of the component.
  • Return

    • Object|Array - Object of the mapping between real keys and dynamic keys. Alternatively, an array of real keys. Concrete keys are the actual keys found in the store. Dynamic keys are the property names of the props accessible in the component. A dynamic name is like an alias of its corresponding concrete key which only lives in a component.

Store(data)

The Store class. It implements a flat key/value store with the support of subscribing changes of keys with listening functions. You do not need it for most of the cases.

  • Methods
    • constrcutor(data)
      • Parameters
        • data - optional Object: The initial data for the store.
    • set(key, value) Change a value or values in the store.
      • Parameters
        • key - required String|Object: String value of the key. Alternatively, Object of key/value pairs when you need to set multiple values at once.
        • value - optional Any: The value of the key.
    • get(key) Get value or values from the store.
      • Parameters
        • key - required String|Array: String value of the key. Alternatively, an array of keys when you need to retrieve multiple values at once.
      • Return
        • Any the value of the key or an object which contains the key/value pairs if the key was an array.
    • subscribe(keys, listener, maps) Call the listening function when set was called on any of the keys.
      • Parameters
        • keys - required Array: Array of keys the listener will be subscribing to.
        • listener - required Function: The listener function.
        • maps - optional Object: Optional injection key to real key mappings.
    • unsubscribe(listener) Unsubscribe the function from all the keys it's listening to.
      • Parameters
        • listener - required Function: The listener function to unsubscribe.

License

MIT