Reducer and action classes

4 minutes read

Applies to 1.1.x
State DRAFT

The "hypereact" state library includes pre-defined managed reducers that enable developers to just focus on the actions design and implementation. The built-in reducers are ready-to-use and the general approach is based on:

  • the built-in reducers implement pre-defined reduce functions that process actions with pre-defined logic (based on the reducer class used)
  • the pre-defined logic is based on the implementation of the action itself (properties, functions, decorators) that includes everything needed to change the stateĀ 
  • developers can keep their code clean and maintainable cause there is no more reducer to be maintained and every new action can be implemented as a self-consistent atomic operation

Reducers and configuration

The store manager, when initialized, accepts a configuration object that must match the "IReduxConfig" interface. Specifically the configuration object is used to define the reducer that will handle each state slices.

// redux configuration is based on slice keys as property names and reducer implementations as values
export interface IReduxConfig {
  [slice: string]: IReducer<any>;
}

The reducer must be implemented as classes that honor the IReducer interface. Even if the "hypereact" state library provides developers with built-in reducers, a generic conventional reducer can be implemented as follows.

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0
};

// reducer as class that implements the IReducer<T> interface (with T equal to the state slice type)
class CounterReducer implements IReducer<CounterState> {

  // traditional reduce function
  reduce(state: CounterState = initialState, action: any): CounterState {
    switch (action.type) {
      case "COUNT_INCREMENT":
        return { value: state.value + 1 };
      case "COUNT_DECREMENT":
        return { value: state.value - 1 };
      default:
        return state;
    }
  }
}

// redux configuration object that binds the counter state slice with the CounterReducer reducer class
const initialReduxConfig: IReduxConfig = {
  "counter": new CounterReducer()
};

const storeManager = StoreManager.getInstance(initialReduxConfig);

// dispatch old-fashioned action object
storeManager.dispatch({
  type: "COUNT_INCREMENT"
});

// dispatch old-fashioned action object 
storeManager.dispatch({
  type: "COUNT_DECREMENT"
});

Built-in reducers

The ready-to-be used reducers included in the library are:

  • ReduceableReducer: executes the reduce function implemented within the action
  • MergeableReducer: merges all the action properties into the state slice

All the built-in reducers are also available in the equivalent persistence-enabled implementation: PersistentReduceableReducer, PersistentMergeableReducer.

Reduce-able reducer

The ReduceableReducer supports "self-reduceable" action classes. The "self-reduceable" action classes must implement the IReduceableAction<T> interface that requires a reduce(state: T) function.

When using a ReduceableReducer class, developers can just proceed on:

  • defining properties required for reducing the action (properties)
  • define a reduce function that will be executed when the action is dispatched

It is important to notice that only the reduce function is allowed within the action class. The store manager ensures that the action is dispatched as a POJO and the reduce function is called with the this keyword referred to it.

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0
};

// reduceable action class decorated to set type and slice binding
@ReduxAction("COUNTER_INCREMENT", "counter")
export class CounterIncrement implements IReduceableAction<CounterState> {

  // the reduce function is executed on action dispatch
  //   the state argument is set the a new state
  //   the this keyword refers to the action POJO representation)
  reduce(state: CounterState) {
    state.value++;
    return state;
  }
}

// reduceable action class without decorators
export class CounterDecrement implements IReduceableAction<CounterState> {
  type: string = "COUNTER_DECREMENT";
  slice: string = "counter";

  reduce(state: CounterState) {
    state.value--;
    return state;
  }
}

const initialReduxConfig: IReduxConfig = {
  "counter": new ReduceableReducer(initialState)
};

const storeManager = StoreManager.getInstance(initialReduxConfig);

storeManager.dispatch(new CounterIncrement());

storeManager.dispatch(new CounterDecrement());

Merge-able reducer (experimental)

The MergeableReducer supports "patching" action classes. The "patching" action classes are just merged against the state.

When using a MergeableReducer class, developers are just required to define actions with same propertiescset to the values they want to change in the state slice.

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0
};

// reduceable action class decorated to set type and slice binding
@ReduxAction("COUNTER_INCREMENT", "counter")
export class CounterIncrement {
  value: number;

  // constructor is used for eventual current state retrieval
  constructor() {
    const currentState: CounterState = StoreManager.getInstance().getState(this.slice);
    this.value = currentState.value + 1
  }
}

// reduceable action class without decorators
export class CounterDecrement {
  type: string = "COUNTER_DECREMENT";
  slice: string = "counter";

  value: number;

  constructor() {
    const currentState: CounterState = StoreManager.getInstance().getState(this.slice);
    this.value = currentState.value + 1
  }
}

const initialReduxConfig: IReduxConfig = {
  "counter": new MergeableReducer(initialState)
};

const storeManager = StoreManager.getInstance(initialReduxConfig);

storeManager.dispatch(new CounterIncrement());

storeManager.dispatch(new CounterDecrement());