grafana/style_guides/redux.md
2019-03-18 06:39:34 -07:00

4.7 KiB

Redux framework

To reduce the amount of boilerplate code used to create a strongly typed redux solution with actions, action creators, reducers and tests we've introduced a small framework around Redux.

+ Much less boilerplate code - Non Redux standard api

New core functionality

actionCreatorFactory

Used to create an action creator with the following signature

{ type: string , (payload: T): {type: string; payload: T;} }

where the type string will be ensured to be unique and T is the type supplied to the factory.

Example

export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();

// later when dispatched
someAction('this rocks!');
// best practices, always use an interface as type
interface SomeAction {
  data: string;
}
export const someAction = actionCreatorFactory<SomeAction>('SOME_ACTION').create();

// later when dispatched
someAction({ data: 'best practices' });
// declaring an action creator with a type string that has already been defined will throw
export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();
export const theAction = actionCreatorFactory<string>('SOME_ACTION').create(); // will throw

noPayloadActionCreatorFactory

Used when you don't need to supply a payload for your action. Will create an action creator with the following signature

{ type: string , (): {type: string; payload: undefined;} }

where the type string will be ensured to be unique.

Example

export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();

// later when dispatched
noPayloadAction();
// declaring an action creator with a type string that has already been defined will throw
export const noPayloadAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create();
export const noAction = noPayloadActionCreatorFactory('NO_PAYLOAD').create(); // will throw

reducerFactory

Fluent API used to create a reducer. (same as implementing the standard switch statement in Redux)

Example

interface ExampleReducerState {
  data: string[];
}

const intialState: ExampleReducerState = { data: [] };

export const someAction = actionCreatorFactory<string>('SOME_ACTION').create();
export const otherAction = actionCreatorFactory<string[]>('Other_ACTION').create();

export const exampleReducer = reducerFactory<ExampleReducerState>(intialState)
  // addMapper is the function that ties an action creator to a state change
  .addMapper({
    // action creator to filter out which mapper to use
    filter: someAction,
    // mapper function where the state change occurs
    mapper: (state, action) => ({ ...state, data: state.data.concat(action.payload) }),
  })
  // a developer can just chain addMapper functions until reducer is done
  .addMapper({
    filter: otherAction,
    mapper: (state, action) => ({ ...state, data: action.payload }),
  })
  .create(); // this will return the reducer

Typing limitations

There is a challenge left with the mapper function that I can not solve with TypeScript. The signature of a mapper is

<State, Payload>(state: State, action: ActionOf<Payload>) => State;

If you would to return an object that is not of the state type like the following mapper

mapper: (state, action) => ({ nonExistingProperty: ''}),

Then you would receive the following compile error

[ts] Property 'data' is missing in type '{ nonExistingProperty: string; }' but required in type 'ExampleReducerState'. [2741]

But if you return an object that is spreading state and add a non existing property type like the following mapper

mapper: (state, action) => ({ ...state, nonExistingProperty: ''}),

Then you would not receive any compile error.

If you want to make sure that never happens you can just supply the State type to the mapper callback like the following mapper:

mapper: (state, action): ExampleReducerState => ({ ...state, nonExistingProperty: 'kalle' }),

Then you would receive the following compile error

[ts]
Type '{ nonExistingProperty: string; data: string[]; }' is not assignable to type 'ExampleReducerState'.
  Object literal may only specify known properties, and 'nonExistingProperty' does not exist in type 'ExampleReducerState'. [2322]

New test functionality

reducerTester

Fluent API that simplifies the testing of reducers

Example

reducerTester()
  .givenReducer(someReducer, initialState)
  .whenActionIsDispatched(someAction('reducer tests'))
  .thenStateShouldEqual({ ...initialState, data: 'reducer tests' });