Cleaning up reducers with kotlin-when

Introduction

In React, Redux reducers play a pivotal role in managing the application’s state. They are responsible for specifying how the application’s state should change in response to various actions or events.

Reducers are pure functions that take the current state and an action as input and return a new state object, ensuring that the original state remains unaltered. This predictable and immutable approach to state management makes Redux a popular choice for handling complex application state, enabling developers to easily reason about the flow of data and maintain a consistent, centralized state throughout their React applications.

The verbose reducer

Consider a simple example of a Redux reducer for managing the state of data fetching from an API. This reducer might have three states: ‘idle’, ‘loading’, and ‘loaded’. In the ‘idle’ state, no data has been requested or received, and the initial state is empty.

When an action is dispatched to initiate a data request, the reducer transitions to the ‘loading’ state, indicating that data retrieval is in progress. Once the data is successfully fetched, the state transitions to ‘loaded,’ storing the received data.

If an error occurs during the request, a separate state like ‘error’ could be included to handle error conditions. This basic example showcases how a Redux reducer can efficiently manage the progression of data fetching.

// Initial state
const initialState = {
  data: null,
  loading: false,
  error: null,
};

// Reducer function
const dataReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return {
        ...state,
        loading: true,
      };
    case 'FETCH_DATA_SUCCESS':
      return {
        ...state,
        loading: false,
        data: action.payload,
        error: null,
      };
    case 'FETCH_DATA_FAILURE':
      return {
        ...state,
        loading: false,
        data: null,
        error: action.error,
      };
    default:
      return state;
  }
};

export default dataReducer;

Cleaning it up

So how can we go about cleaning up this reducer? This is where our kotlin-when package comes in handy. In order to clean up our state we can do something like this.

import { when } from 'kotlin-when'
// Initial state
const initialState = {
  data: null,
  loading: false,
  error: null,
};

// Reducer function
const dataReducer = (state = initialState, action) => when(action.type, {
  'FETCH_DATA_REQUEST': () => ({...state, loading: true }),
  'FETCH_DATA_SUCCESS': () => ({...state, loading: false, data: action.payload }),
  'FETCH_DATA_FAILURE': () => ({...state, loading: false, data: null, error: action.error  }),
  // the else clause is similar to the default clause in switch
  else: () => state,
});


export default dataReducer;

As we can see our function looks a whole lot smaller now and we can still handle the same states. Now let’s see if we can clean this up even further.

Easier testing

By default when returns the value passed at the end of its function, in this case it will pass back each state given to it by the return function in our when conditions. So we could also move these functions out of the clause itself and have them to be tested individually or simply write tests for this method passing in the different keys. Here is an example jest test.

const when = require("./whenjs");

const initialState = { data: null, loading: false }
const reducer = (state = initialState, action) => when(action.type, {
  'FETCHING': () => ({ ...state, loading: true }),
  'FETCHING_DONE': () => ({ ...state, loading: false, data: [1, 2, 3] }),
})

describe("When.js", () => {
  it("should test a 'reducer method'", () => {
    const result = reducer(initialState, { type: 'FETCHING' });
    expect(result.loading).toBe(true);
  });
  it("should test a 'reducer method'", () => {
    const result = reducer(initialState, { type: 'FETCHING_DONE' });
    expect(result.loading).toBe(false);
    expect(JSON.stringify(result.data)).toBe(JSON.stringify([1, 2, 3]));
  });
});

Conclusions

While not absolutely needed in every project this is a nice tool to have in your toolkit that can clean your code up. It’s also slightly faster than a switch statement as it doesn’t have to check each case but rather accesses the correct function in O(1) time.