Skip to content
Valentin Mourot edited this page May 2, 2019 · 5 revisions

Redux

Redux is a store that handles and manage global application state. This global state is split into smaller units that corresponds to each module (including application modules, Timeline modules, and Mobile Framework modules).

Despite the store has independent parts for each module, all the state is readable everywhere in read-mode. Alter the state can be done using actions and action types.

This section is about how to add Redux feature to your application module.

An application module can use the Redux store by defining:

  • A reducer
  • Some action types
  • Some actions (a.k.a. "thunks")

1. Create a reducer

You may create a reducer folder at the root of your application module directory, with an index.ts inside. The structure of your reducer is up to you, but we consider using the way Redux documentation explain it: combined sub-reducers with switch statement.

It's also the right place to define the types of the state handled by the reducer.

2. Create action types

Each action type is a unique string in the SCREAMING_SNAKE_CASE. The reducer can handle actions by identify their type (for example with a switch statement) and return the new state.

You may write you action types in a actionTypes folder at the root of your application module directory. Consider using the same directory structure than your reducer directory.

The right way to name action types is with a subject and a verb. Put the subject before the verb to make a sort of namespacing.

Don't CREATE_USER.
Do USER_CREATE.
Hard for distinguish between many "CREATE_*" action types across the Mobile Framework

Don't messagesClear.
Do MESSAGES_CLEAR.
Action types are string constants, not functions or variables. You may use SCREAMING_SNAKE_CASE.

Don't MESSAGE_RECEIVE.
Do MESSAGE_FETCH_RECEIVE.
Consider put "FETCH" or "ASYNC" between subject and verb to clearly distinguish between sync and async actions.

In addition to this, all your action types will be prefixed with your application module by using the module config thet you created with the moduleTool:

import moduleConfig from "../config";

export const LOGIN_ASYNC_REQUEST = moduleConfig.createActionType("LOGIN_ASYNC_REQUEST");
export const LOGIN_ASYNC_SUCCESS = moduleConfig.createActionType("LOGIN_ASYNC_SUCCESS");
export const LOGIN_ASYNC_FAILURE = moduleConfig.createActionType("LOGIN_ASYNC_FAILURE");
export const LOGIN_ASYNC_CANCEL  = moduleConfig.createActionType("LOGIN_ASYNC_CANCEL");
// [...]

It's also the right place to define the data that will be dispatched into your Redux actions :

export interface I_LOGIN_ASYNC_REQUEST {
  // [...]
}
export interface I_LOGIN_ASYNC_SUCCESS {
  userId: string;
  login: string;
  // [...]
}
// [...]

3. Create actions and thunk actions

Create a actions folder at the root of your application module directory. Inside, you may declare some action creators :

You can also define here some action creators :

// Public actions creators must be prefixed with "action_".
// Expose public actions that can be called with dispatch securely. (Generally thunks, or actions that do not rely on async functions)
export action_dummyTask() => {
  type: DUMMY
};

// Private action creators with "_action_".
// Don't call dispatch() with a private action yourself outside a thunk action.
// Private actions generally rely on async function and are orchestrated by a public thunk action.
export _action_loginAsyncSuccess(userData: I_LOGIN_ASYNC_SUCCESS) => {
  type: LOGIN_ASYNC_SUCCESS,
  userData
};

And some thunk actions:

export function action_login(credentials?: { username: string; password: string }) {
  return async (dispatch, getState) => {
    try {
      dispatch({ type: LOGIN_ASYNC_REQUEST }); // Simple action without action creators can be dispatched directly
      // [...]
    } catch (e) {
      dispatch(_action_loginAsyncFailure(e.code)); // Or dispatch actions with action creator
    }
  };
}

4. Use prebuilt actions & reducers

Some action creators and reducers are provided by Mobile Framework, so you don't have to code all yours from scratch. They all are living in /app/infra/redux.

Async reducer

The Async reducer can handle any array of data that is fetched from a distant server. The Async reducer is not designed to handle paginated data.

This tool is obsolete. We are planning to write a new set of prebuilt reducers and action creators in a smarter way.

The Async reducer is made of action creators, thunks and a reducer creator. To use it, you need to surcharge action creators and give to the reducer creator your own one.

4 actions are handled : request, receive, fetch error and invalidated data. Async reducer is designed to not more fetch data until it is invalidated manually, or using a forceFetch action.

1. Create you action types
import { asyncActionTypes } from "../../infra/redux/async";

export const actionTypes = asyncActionTypes(
  homeworkConfig.createActionType("DIARY_LIST")
);
2. Create custom action creators
export function _action_dummyDataListInvalidated() {
  return { type: actionTypes.invalidated };
}

export function _action_dummyDataListRequested() {
  return { type: actionTypes.requested };
}

export function _action_dummyDataListReceived(data: IDummyDataList) {
  // Here provide a `data` property and a `receivedAt` to your action.
  return { type: actionTypes.received, data, receivedAt: Date.now() };
}

export function _action_dummyDataListFetchError(errmsg: string) {
  return { type: actionTypes.fetchError, error: true, errmsg };
}
3. Use the prebuild thunk action to fetch data.

Async redux tools also provide a fetch wrapper function : asyncGetJson(). Give it the url to fetch, and an adapter function that will convert the data from received data type to redux store data type.

// Create a function that returns your dummy data list state from the global state of your Redux Store
const localState = globalState =>
  moduleConfig.getLocalState(globalState).dummyDataList;

// Create your force-fetch function
export function action_fetchDummyDataList() {
  return async (dispatch, getState) => {
    dispatch(_action_dummyDataListRequested());

    try {
      // Here is the fetch wrapper
      const data = await asyncGetJson(
        "/dummy/list", // See "Fetch" section of this documentation
        rawData => rawData.isError ? [] : rawData.array
      );
      dispatch(_action_dummyDataListReceived(data));
    } catch (errmsg) {
      dispatch(_action_dummyDataListFetchError(errmsg));
    }
  };
}

// And create tour fetch-if-needed function
export function action_fetchDummyDataListIfNeeded() {
  return asyncFetchIfNeeded(localState, action_fetchDummyDataList);
}
4. Create the reducer

Write a reducer that handle the received data, then create a async reducer from it :

import { IArrayById } from "../../infra/collections";
import asyncReducer from "../../infra/redux/async";
import { actionTypes } from "../actions/dummyData"; // Your actions create just before

// TYPE DEFINITIONS -------------------------------------------------------------------------------

export interface IDummyData {
  id: string;
  title: string;
  content: string;
}

export type IDummyDataList = IArrayById<IDummyData>;
// We use Mobile Framework custom collection types.
// See "Collections" section of this documentation.
// Here, IDummyDataList = { [id: string]: IDummyData }.

// THE REDUCER ------------------------------------------------------------------------------------

const stateDefault: IDummyDataList = {}; // Default state for the Redux store

const dummyDataListReducer = (
  state: IDummyDataList = stateDefault,
  action
) => {
  switch (action.type) {
    case actionTypes.received:
      return action.data;
    default:
      return state;
  }
};

export default asyncReducer<IDummyDataList>(
  dummyDataListReducer,
  actionTypes
);

Clone this wiki locally