Name Redux Actions After States, Not Getters and Setters

Name Redux Actions After States, Not Getters and Setters

If you're using your Redux actions like getters and setters, you might as well be using something else because you are not using Redux to its full potential. Dan Abramov seems to agree:

It does not help that the official Redux documentation promotes the setter pattern. Consider this example where a React component sets a visibility filter to a filter argument.

import {
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

For React-Redux beginners, I think this is a great illustrative example, but I think it sets up developers to think Redux actions are designed to be used like getters and setters. When React apps are small, getters and setters will get you by, but as you add more reducers you will notice that some front end state changes will require several getters and setters.

An Example: Scanning for Devices on the Local Network

Consider this example:

  1. Our task is to scan for devices on the local network by IP address and store those devices. For instance, let's say these devices are Wi-Fi enabled switches (e.g. the TP-Link HS100), computer servers, or even satellites!
  2. If a device is selected, update that selected device's IP address.
  3. While the device scanning process is happening, display a loading bar. Once the scan is completed, the loading bar disappears.

Before: Using Redux Actions as Getters and Setters

This is how I wrote the reducers/index.js code before using getters and setters; the code has been simplified significantly so we can focus on the relevant points:

// Point #1 from above.
const devices = (state = [], action) => {
  switch (action.type) {
    case 'DEVICES__UPDATE':
      return action.devices;
    ...
  }
};

// Point #2 from above.
const selectedDevice = (state = {id: null, ip: null, name: null}, action) => {
switch (action.type) {
    case 'SELECTED_DEVICE__UPDATE':
      return Object.assign({}, state, action.payload);
    ...
};

// Point #3 from above.
const deviceSetupInitialState = {
  isScanning: false,
};
const deviceSetup = (state = deviceSetupInitialState, action) => {
  switch (action.type) {
    case 'HIDE_SCANNING_BAR':
      return { ...state, isScanning: false };
  }
  ...
};
reducers/index.js with getters and setters. Not good :(

The first block of this code snippet uses a setter to update the list of devices returned by the device scanner function. The second block is a setter used to update the currently selected device: we need this because the device's IP may change between scans. The third block is based on a verb, "hide scanning bar", which is called when the device scan is completed. Below is the code that was written in the device scanning component that called these Redux actions.

scanForDevices(
  (device) => {
    this.props.updateDevices(devices);
    const selectedDevice = devices.find((device) => device.id === this.props.selectedDevice.id)
    this.props.updateSelectedDevice({ selectedDevice });
    this.props.hideScanningBar();
  }
);
DeviceDiscovery.js calling the Redux getters and setters :(

These are many problems with the scanForDevices snippet above:

  1. If there were multiple components with scanning functionality, these four lines would have to be copied and pasted.
  2. This code requires 3 redux actions to be connected (updateDevices, updateSelectedDevice, and hideScanningBar), as well as 1 redux state (this.props.selectedDevice.id).
  3. The programmer shouldn't have to instruct line-by-line in the React component how to handle a scan for devices.
  4. Lastly, it's 4 lines and it could be shorter.

After: Naming Redux Actions Based on States

Let's look at the improvement:

const devices = (state = [], action) => {
  switch (action.type) {
    case 'DEVICE_SCAN_COMPLETED':
      return action.devices;
    default:
      return state;
  }
};

const selectedDevice = (state = {id: null, ip: null, name: null}, action) => {
switch (action.type) {
  switch (action.type) {
    case 'DEVICE_SCAN_COMPLETED':
      const device = action.devices.find((device) => device.id === state.id && device.isVPN === state.isVPN);
      if (device) {
        return { ...state, ip: device.ip };
      }
      return state;

const deviceSetupInitialState = {
  isScanning: false,
};
const deviceSetup = (state = deviceSetupInitialState, action) => {
  switch (action.type) {
    case 'DEVICE_SCAN_COMPLETED':
      return { ...state, isScanning: false };
  }
  ...
};
reducers/index.js with actions based on states. Better! :)

Notice how there is only one action type here: DEVICE_SCAN_COMPLETED. Now take a look at the corresponding DeviceDiscovery.js component.

scanForDevices(
  (device) => {
    this.props.deviceScanCompleted(devices);
  }
);
DeviceDiscovery.js after naming actions based on states :)

Here, the programmer just has to write this.props.deviceScanCompleted(devices) which leaves the logic to the Redux store to trigger updates to devices, selectedDevice, and deviceSetup. This is also beneficial because if in the future we expect other changes to take place when a device scan is completed, we can continue the pattern of using DEVICE_SCAN_COMPLETED.

Other Examples

Consider another feature that when a user disconnects from the device by clicking on the "Disconnect from Device" button, Redux resets visualization, health, cameras, instruments, notifications, selected device, device connection, and much more. If you follow the getter/setter approach, you will have a resetSelectedDevice() -like function for every single store listed above that needs resetting. That's a lot of importing, a lot of lines, and a lot of code duplication since there are multiple buttons that can disconnect from the device. Instead, by using states, you can just call a single action: this.props.deviceDisconnected and handle that action type in all of the Redux reducers. Much cleaner right?

Conclusion

In conclusion, Redux actions should be treated as states, not getters and setters. The benefits aren't obvious when your React app is small, but as your code base grows, you will be set up for success by moving away from getters and setters now. Of course, this is only my opinion, so I would love to hear your thoughts in the comments below.