Skip to content

Adding Redux to a plugin

Louise Davies edited this page Feb 18, 2020 · 7 revisions

The root app uses Redux for it's state management, the plugins don't have to as events will be fired via the browser - but it is the recommended tool for a complex application and helps decouple the view from the state.

The tutorials on the Redux site are a good starting point but we will also outline the steps here. You can go through the changes for adding Redux to the parent app at commit 8cb635 and then the changes to add testing to it at commit 4ea12.

Read one-way dataflows to understand the data flow model first and the various parts needed for React with Redux

Steps to add Redux

Installation

Install the packages needed:

yarn add redux redux-thunk redux-logger [email protected] @types/react-redux @types/redux-logger

Types

Create a new state folder in your src folder, make a new actions.types.tsx file and add a type for a new action

export const {{action}}Type = '{{string identifying action}}';

export interface {{action}}Payload {
  {{property}}: {{propertyType}}
}

where action, string identifying action, property and propertyType need to be defined.

Additionally, make a app.types.tsx file to store any non-action types, and you will need to define some overall types:

import { ThunkAction } from 'redux-thunk';
import { AnyAction } from 'redux';

export interface AppState {}

export interface StateType {
  app: AppState;
}

export interface ActionType<T> {
  type: string;
  payload: T;
}

export type ThunkResult<R> = ThunkAction<R, StateType, null, AnyAction>;

AppState will be the custom state object of the application, so add any new state items to this type, and the StateType is the overall type of the redux store - these are separate so that we can store other things (e.g. the router state) separate from our custom state. The other types are some generic types for Actions and Thunks which are needed to define our own actions and thunks.

Actions

Add a new actions folder inside the state folder and start building actions:

import { NotificationType, NotificationPayload } from '../daaas.types';
import { ActionType } from '../state.types';

export const daaasNotification = (
  message: string
): ActionType<NotificationPayload> => ({
  type: NotificationType,
  payload: {
    message,
  },
});

The key here is that any action should be of the format { type: {{some string}}, payload: {{some object}} }, following this convention will allow the redux-logger library to output actions to the console as well as making it easier for the redux dev tools to integrate with your actions.

Reducers

Create a folder inside state called reducers and add reducers there. Typically a reducer takes the form:

function reducer(state = initialState, action) {
  switch (action.type):
    case 'TYPE1':
      return {{update the state somehow}}

    default:
      return state
}

where the default case is important because an action is sent to every reducer and it is up to the reducer to decide if it wants to respond to the action.

The switch statement can get quite large over time, Redux provides several helper functions for reducing boilerplate code here - we make use of the createReducer function in the root app.

As well as defining your own reducers, typically you will want a top level reducer that brings them all together and gives your state some structure - here is an example App.reducer:

import { combineReducers } from 'redux';
import daaasReducer from './daaas.reducer';

const AppReducer = combineReducers({
  daaas: daaasReducer,
});

export default AppReducer;

This is a good way of composing different reducers for different parts of the state; you can also chain reducers to stop any single one getting too long (the action simply trickles down the chain and only reducers that are interested affect the state).

One area of caution - if multiple reducers respond to an action then you have to be careful about the order they are applied and how that affects your state; if it's different parts of the state (e.g. set a loading flag as well as start loading data) then you don't need to worry.

Connected Components

The final part of the cycle is then connecting components to the state. Here's an example component:

import React from 'react';
import { connect } from 'react-redux';

const ExampleComponent = props => (
  <div>{props.notifications}</div>
);

const mapStateToProps = state => {
  return {
    notifications: state.daaas.notifications,
  };
};

export default connect(mapStateToProps)(ExampleComponent);

The react-redux library will automatically handle calling the update mechanism. Similarly mapDispatchToProps can be used to push actions round the one-way data flow (e.g. when a user clicks a button it dispatches an action). As an example:

const mapDispatchToProps = dispatch => ({
  clickHandler: () => dispatch(someAction()),
  updateText: (text) => dispatch(updateTextAction(text))
})

and then it's added to the component with connect(mapStateToProps, mapDispatchToProps), from buttons you can then hook up onClick={props.clickHandler}.

Stitching it all together

The previous steps provide all the parts, the final stage is to hook it all together in index.tsx:

const middleware = [thunk];
const store = createStore(
  AppReducer,
  applyMiddleware(...middleware)
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

where an actual store is created and passed to a Provider.

Clone this wiki locally