Skip to content

Latest commit

 

History

History
239 lines (194 loc) · 6.98 KB

reducers.md

File metadata and controls

239 lines (194 loc) · 6.98 KB

Reducers

Props

Reducers in Flambeau are differentiated from the standard Redux reducer by a number of modest features. One of these is the addition of props for reducers.

These props allow both the initial state and the state transformation during an action to be customized. Like React’s props, their use is encouraged to enable reducers to be less hard-coded and more reusable.

Initial State

The initial state in Flambeau is determined by a declaration of the exported function getInitialState. The props are passed as the first argument.

export function getInitialState({ initialItems = [] }) {
  return {
    items: initialItems
  };
}

Implementing Action Responders

Responding to actions is done in a similar manner to action creators: by declaring functions. The name of the function mirrors that of the action creator. The functions are declared in a vanilla JavaScript object, named the same as the action set.

The only difference from action creators is the current state is passed as the first argument.

export const TodoListActions = {
  addTodo(state, { text }) {
    return state.concat({ text });
  }
}

Composing

Reducers can be easily composed within each other. This allows you to break your reducers into multiple pieces.

Forwarding actions is possible, especially easily done in bulk per action set. To forward an action, declare an action set as a function instead of an object. When an action from this set is dispatched, this function will be called with the following parameters.

  • isAction: when the payload being dispatched is a standard action.
  • isIntrospection: when introspection into reducers is being requested.
  • actionID: the identifier of the action or introspection method.
  • payload: the payload being dispatched.
  • props: the props of this particular reducer.
  • forwardTo(): Call this to use another reducer on a subset of your state.
// TodoItemActions.js

export function changeText({ text, index }) {}
export function changeCompleted({ isCompleted, index }) {}
// TodoListReducer.js
import TodoItemReducer from './TodoItemReducer';

export function getInitialState() {
  return {
    items: []
  };
}

export function TodoItemActions(state, { isAction, isIntrospection, payload, props, forwardTo }) {
  if (isAction) {
    const { index } = payload;
    state = Object.assign({}, state, {
      items: state.items.slice() // Make a copy of the entire array
    });
    state.items[index] = forwardTo({ responder: TodoItemReducer, initialState: state.items[index] });

    return state;
  }
}
// TodoItemReducer.js

export const TodoItemActions = {
  changeText(item, { text, index }) {
    return Object.assign({}, item, { text });
  },

  changeCompleted(item, { isCompleted, index }) {
    return Object.assign({}, item, { isCompleted });
  }
}

Introspection

An application will often need different actions to be dispatched depending on the store’s state. Differing from the normal method of directly checking the store (getState() in Redux), Flambeau introduces introspection methods, which allow reducers to completely encapsulate its state from the outside world.

Say a todo list allows importing items from a URL online. The import action creator may want to only load data if it hasn’t been done already. Because action creators are stateless, this bit of information will be stored by a reducer somewhere.

Introspection methods allow a reducer to declare its preference, say whether to load a URL or not, whilst leaving the implementation details of the store’s state hidden from the action creator.

// TodoListActions.js
import fetch from 'isomorphic-fetch';

export function addTodosFromURL({ items, URL }) {}

function importTodosFromURL({ URL }, { currentActionSet }) {
  fetch(URL)
  .then(response => response.json())
  .then(items => currentActionSet.addTodosFromURL({ items, URL }));
}

export function importTodosFromURLIfNeeded({ URL }, { currentActionSet }) {
  if (!currentActionSet.consensus.hasImportedFromURL({ URL }).every()) {
    // This function is not exported as a public action, instead used directly.
    importTodosFromURL({ URL }, { currentActionSet });
  }
}

export const introspection = {
  hasImportedFromURL({ URL }) {}
};
// TodoListReducer.js
export function getInitialState({ initialItems = [] }) {
  return {
    items: initialItems,
    importedURLs: {}
  };
}

export const TodoListActions = {
  addTodosFromURL(state, { items, URL }) {
    // Other reducers might have returned false from hasImportedFromURL()
    if (state.importedURLs[URL]) {
      return;
    }

    return Object.assign({}, state, {
      importedURLs: Object.assign({}, state.importedURLs, { [URL]: true }),
      items: state.items.concat(items)
    });
  },

  introspection: {
    hasImportedFromURL(state, { URL }) {
      return Boolean(
        state.importedURLs[URL]
      );
    }
  }
};

Introspection Consensus

The consensus property is part of every connected action set, including those passed to action creators (currentActionSet and allActionSets), that have introspection methods.

To use it, append your introspection identifier, and call it with the payload to pass to each reducer’s introspection method. Then call one of the following methods:

  • some([callback]): like Array.some, returns true if callback returns true for any reducer’s result. If a callback is not passed, then the result is treated as a boolean.
  • every([callback]): like Array.every, returns true if callback returns true for every reducer’s result. If a callback is not passed, then the result is treated as a boolean.
  • singleton(): expects there to be only one reducer, returning its result. Throws an exception if zero or more than one reducer responded.
  • reduce(callback[, initialValue]): like Array.reduce, combines every reducer’s result using a callback passed the combined result so far, and the currently iterated reducer’s result for the introspection method.
  • toArray(): returns an array of results of every reducer for this introspection method.
if (currentActionSet.consensus.yourIntrospectionID({
  yourPayloadProperties: true
}).some()) {
  // If any reducer returned true.
}
if (currentActionSet.consensus.yourIntrospectionID({
  yourPayloadProperties: true
}).every()) {
  // If all reducers returned true.
}
const currentActionSet.consensus.yourIntrospectionID({
  yourPayloadProperties: true
}).singleton();
// A single chosen reducer returned a value.
// Throws if no or multiple reducers returned a result.
// Great for configuration variables.
const combinedResult = currentActionSet.consensus.yourIntrospectionID({
  yourPayloadProperties: true
}).reduce((combined, current) => {
  // Reduce `combined` and `current`
  return combined + current;
}, /* optional initialValue */ 0);

Actions · Reducers · Using with Redux