:
Last modified: Sep 8, 2021

Redux

Guidelines for using Redux in Altinn Studio

Redux is used to manage the states of the applications. The components update the states in the Redux store, instead of keeping state only within the component. This allows components to easily react to state changes from other components.

Redux Toolkit

As of 2021, we have started using Redux Toolkit rather than the traditional Redux setup. This allows us to get rid of a lot of boilerplate code. Read more about Redux Toolkit here.

In practice, this affects the setup of actions and reducers. We currently use Redux Toolkit in Altinn Studio, and plan to update the App frontend as well. Redux Toolkit also works well in parallel with the more traditional setup. In App frontend, any new reducers should be set up as slices where possible, using Redux Toolit. New actions can be set up using the createAction method, rather than the existing boilerplate setup.

createAction

React more about creating actions here. Actions can also be created automatically within slices. The createAction command is useful for actions that are are not necessarily handled by the reducer.

createSlice

Read more about slices here. Slices combine the creation of actions and reducers into one command, thereby reducing the amount of code, as well as making the setup much more readable.

Dispatching actions

In the traditional setup, we use an action dispatcher. For App frontend, it is possible to combine f.ex. createAction with existing action dispatchers. Where possible, however, actions should be dispatched using the dispatch function, from the component itself. For class components, this function is provided as a prop from mapStateToProps. For functional components using hooks, we can use const dispatch = useDispatch() where the useDispatch function comes from the react-redux package.

Examples

For examples of Redux Toolkit setup in the code, look at the Studio code, where all actions/reducers/dispatching has been updated to use Redux Toolkit.

Basic concepts

Below is a short descriptions of some of the basic Redux concepts that we use. Note that these are described using the traditional Redux setup and not Redux Toolkit, but the concepts are the same in both cases.

Store

This is where the overall states are stored. Each state is stored here as an object.

Actions

Actions are the events that are triggered when a change is made. An action contains the action type, and any metadata needed to complete the action. For example:

export interface IUpdateFormData extends Action {
  field: string;
  data: any;
  componentId: string;
}

export function updateFormData(field: string, data: any, componentId: string): IUpdateFormData {
  return {
    type: actionTypes.UPDATE_FORM_DATA,
    field,
    data,
    componentId,
  };
}

In this example, a change in form data can trigger the updateFormData action, which contains an object with the action type, as well as the data/metadata.

Sagas

Redux saga is the middleware used to process information before the store is updated (i.e. before the reducer does its work).

An example is asyncronous calls to backend APIs to get data, or submit data.

Each saga defines methods that complete different tasks, connected to actions. These methods are called via listeners that listen to the actions that are being dispatched. There are different sagas for all the different functional areas.

/**
 * Define the saga for the UPDATE_FORM_DATA event
 */
function* updateFormDataSaga(action: ActionType) {
  try {
    // fetch some data
    const relevantData = yield selectRelevantStateObjects(...);
    ...
    // process the data
    const processedData = doRelevantLogic(relevantData);
    ...
    // trigger a new action to update the store
    yield call(updateFormDataSagaFulfilled, processedData, ... );
  } catch (err) {
    yield call(updateFormDataSagaRejected, err);
  }
}

/**
 * Define a listener for the UPDATE_FORM_DATA event
 */
export function* watchUpdateFormDataSaga(): SagaIterator {
  yield takeLatest(FormFillerActionTypes.UPDATE_FORM_DATA, updateFormDataSaga);
}

Reducers

Redux reducers are used to update the different states in the store. There is one reduer per state. The reducers listen to the actions that are dispatched when changes are made, and update the state accordingly.

Example:

const initialState: IFormDataState = {
  formData: {},
  error: null,
};

const FormDataReducer: Reducer<IFormDataState> = (
  state: IFormDataState = initialState,
  action?: Action,
): IFormDataState => {
  if (!action) {
    return state;
  }

  switch (action.type) {
    case actionTypes.UPDATE_FORM_DATA_FULFILLED: {
      const { field, data } = action as IUpdateFormDataFulfilled;
      return Immutable<IFormDataState>(state, {
        formData: {
          $setField: {
            field,
            data,
          },
        },
      });
    }

    case actionTypes.UPDATE_FORM_DATA_REJECTED: {
      const { error } = action as IUpdateFormDataRejected;
      return Immutable<IFormDataState>(state, {
        error: {
          $set: error,
        },
      });
    }

    default: {
      return state;
    }
  }
};

export default FormDataReducer;