Search code examples
reduxreact-reduxnormalizrredux-actions

Redux: What is the best way to toggle a boolean value in a normalized state tree?


I'm currently developing an app with React Native. The state of the app is quite complex, but managable due to Redux and Normalizr. I now have to implement a functionality for the user to filter items.

In order for the user to filter items, I enriched the server response in the Normalizr schema:

export const subCategorySchema = new schema.Entity(
  "subCategories",
  {},
  {
    idAttribute: "uuid",
    processStrategy: entity => {
      const newEntity = Object.assign({}, { name: entity.name, uuid: entity.uuid, chosen: false });
      return newEntity;
    }
  }
);

The corresponding reducer now looks like this:

const initialState = {};
const subCategoriesReducer = (state = initialState, action) => {
  if (action.payload && action.payload.entities) {
    return {
      ...state,
      ...action.payload.entities.subCategories
    };
  } else {
    return state;
  }
};

These the subcategories now get displayed in the UI using this SwitchListItem component, which gets it's items through a selector:

import React, { Component } from "react";
import { Switch, Text, View } from "react-native";
import PropTypes from "prop-types";

import styles, { onColor } from "./styles";

export default class SwitchListItem extends Component {
  static propTypes = {
    item: PropTypes.object
  };

  render() {
    const { name, chosen } = this.props.item;
    return (
      <View style={styles.container}>
        <Text style={styles.switchListText}>{name}</Text>
        <Switch style={styles.switch} value={chosen} onTintColor={onColor} />
      </View>
    );
  }
}

I'm now about to implement the <Switch /> component's onValueChange() function, which is where my question arose:

What is the best way to toggle a boolean value in a normalized state tree?

I came up with two solutions, which I will describe below. Please let me know if you think any one of these is good. If not I would love to get advice on what I could do better :)

Solution 1: Extending the reducer:

My first solution for the problem was to extend the reducer to listen to TOGGLE_ITEM actions. This would look something like this:

const subCategoriesReducer = (state = initialState, action) => {
  switch (action.type) {
    case TOGGLE_ITEM:
      if (action.payload.item.uuid in state) return { ...state, ...action.payload.item };
  }
  if (action.payload && action.payload.entities) {
    return {
      ...state,
      ...action.payload.entities.subCategories
    };
  } else {
    return state;
  }
};

This is my preferred solution as it does not need a lot of code.

Solution 2: Enriching the selector that passes the items to the SwitchList:

The other solution would be to enrich the objects while being passed to the list using a selector with it's key for the state. Then I could create an action that uses this key to update the state like this:

const toggleItem = (item, stateKey) => ({
  type: TOGGLE_ITEM,
  payload: {entities: { [stateKey]: item } }
})

I would love to read an answer, preferably opinionated, if you have a lot of experience with Redux. Also, if you think my way of enriching the data in the normalizr is bad and you can come up with a better way, please let me know! Thank you very much for any advice!


Solution

  • I did it in a completely different way.

    I created an array that holds the uuids of the toggled items. Therefore I only need to look, whether the item is in the toggled array.

    Just like this:

    const initialState = {};
    
    export const byId = (state = initialState, action) => {
      if (action.payload && action.payload.entities && action.payload.entities.itemClassifications) {
        return {
          ...state,
          ...action.payload.entities.itemClassifications
        };
      } else {
        return state;
      }
    };
    
    export const chosen = (state = [], action) => {
      if (action.type === TOGGLE_ITEM && action.meta === ITEM_CLASSIFICATION) {
        if (state.includes(action.payload.uuid)) {
          return state.filter(uuid => uuid !== action.payload.uuid);
        } else {
          return [...state, action.payload.uuid];
        }
      } else {
        return state;
      }
    };
    
    const itemClassificationsReducer = combineReducers({
      byId,
      chosen
    });
    
    export default itemClassificationsReducer;
    
    export const getAllItemClassificationsSelector = state =>
      Object.values(state.itemClassifications.byId);
    export const getAllItemClassificationsNormalizedSelector = state => state.itemClassifications.byId;
    export const getChosenItemClassificationsSelector = state => state.itemClassifications.chosen;
    
    export const enrichAllItemClassificationsSelector = createSelector(
      getAllItemClassificationsSelector,
      itemClassifications =>
        itemClassifications.map(val => ({ ...val, stateKey: ITEM_CLASSIFICATION }))
    );
    
    export const getItemClassificationsFilterActiveSelector = createSelector(
      getChosenItemClassificationsSelector,
      itemClassifications => itemClassifications.length > 0
    );