Redux Reducer Array - Find Object Matches and Merge SubArray Properties

Viewed 58

I am building a keyword generator for an app, where it takes in a list of cities and generates those cities meshed into keyword groups by category.

I have tried doing a few other options with .map on the state and .find to get the matches, but I am struggling with properly merging things to look like my final solution. Any references or help to getting it done would be greatly appreciated.

const generatedKeywords = (state = [], action) => {
switch (action.type) {
    case types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS: {
      return [...state, ...action.payload];
    }
    default: {
      return state;
    }
  }
};

I currently pass in with an action something like:

[
   { 
     cityName: "Los Angeles", 
     keywords: ["Los Angeles HVAC Repair", "HVAC Installation Los Angeles"]
   }, 
   {
     cityName: "Sacramento", 
     keywords: ["Sacramento HVAC Repair", "HVAC Installation Sacramento"]
    }
]

This does the job when I pass in the first action, but subsequent actions end us not merging the objects with the same cityName. This is

[
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "Los Angeles Roofing",
                "Roofers in Los Angeles"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "Sacramento Roofing",
                "Roofers in Sacramento"
            ]
        ]
    },
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "HVAC Company in Los Angeles",
                "Los Angeles HVAC Inspection"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "HVAC Company in Sacramento",
                "Sacramento HVAC Inspection"
            ]
        ]
    }
]

I would like it to look like this:

[
    {
        "cityName": "Los Angeles",
        "keywords": [
            
                "Los Angeles Roofing",
                "Roofers in Los Angeles",
                "HVAC Company in Los Angeles",
                "Los Angeles HVAC Inspection"
            
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            
                "Sacramento Roofing",
                "Roofers in Sacramento",
                "HVAC Company in Sacramento",
                "Sacramento HVAC Inspection"
            
        ]
    },
]
1 Answers

Right now, you're concatenating your new state to your old state array. The desired result is more of a merge operation.

If your state is about having single entries for certain cities or keywords, I think it'll be easier to store Objects rather than Arrays. But it's mostly a matter of taste.

To illustrate, here are two snippets.

1. State as an Array, merge logic inside the reducer

  • Transform to a temporary state that has a Map for all cities, and a Set for all keywords.
  • Loop over the new entries and either merge with existing state, or add a new entry.
  • At the end, transform back in to the desired array format

Note: if your state and payloads are small, you can also use array methods like .includes and .find to discover duplicate entries to merge.

const types = { KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS: 1 };

const generatedKeywords = (state = [], action = {}) => {
  switch (action.type) {
    case types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS: {
      const stateMap = new Map(
        state.map(s => [ s.cityName, new Set(s.keywords) ])
      );
      
      for (const { cityName, keywords } of action.payload) {
        if (stateMap.has(cityName)) {
          const wordSet = stateMap.get(cityName);
          keywords.flat().forEach(word => { wordSet.add(word); });
        } else {
          stateMap.set(cityName, new Set(...keywords));
        }
      }
      
      return Array
        .from(stateMap.entries())
        .map(([ cityName, keywords ]) => ({ cityName, keywords: Array.from(keywords) }));
    }
    default: {
      return state;
    }
  }
};

let state = generatedKeywords();
state = generatedKeywords(state, { type: types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS, payload: data1() })
state = generatedKeywords(state, { type: types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS, payload: data2() })

console.log(state);

// DATA
function data1() { return [
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "Los Angeles Roofing",
                "Roofers in Los Angeles"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "Sacramento Roofing",
                "Roofers in Sacramento"
            ]
        ]
    }
];}

function data2() { return [
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "HVAC Company in Los Angeles",
                "Los Angeles HVAC Inspection"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "HVAC Company in Sacramento",
                "Sacramento HVAC Inspection"
            ]
        ]
    }
];}

2. State as an Object

This state format ensures there's only 1 entry per city. You'll still need a helper to prevent duplicate keywords being added.

  • Spread the old state,
  • Add an entry for every city in the payload
  • When creating those entries, make sure to merge in any existing keywords (state[cityName]?.keywords)

const types = { KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS: 1 };
const uniques = xs => Array.from(new Set(xs));

const generatedKeywords = (state = {}, action = {}) => {
  switch (action.type) {
    case types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS: {
      return {
        ...state,
        ...Object.fromEntries(action.payload.map(
          ({ cityName, keywords }) => [ 
            cityName,
            {
              cityName,
              keywords: uniques(
                keywords.flat().concat(
                  state[cityName]?.keywords ?? []
                )
              )
            }
          ]))
      }
    }
    default: {
      return state;
    }
  }
};

let state = generatedKeywords();
state = generatedKeywords(state, { type: types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS, payload: data1() })
state = generatedKeywords(state, { type: types.KEYWORD_GENERATOR_TOOL_ADD_GENERATED_KEYWORDS, payload: data2() })

console.log(Object.values(state));

// DATA
function data1() { return [
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "Los Angeles Roofing",
                "Roofers in Los Angeles"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "Sacramento Roofing",
                "Roofers in Sacramento"
            ]
        ]
    }
];}

function data2() { return [
    {
        "cityName": "Los Angeles",
        "keywords": [
            [
                "HVAC Company in Los Angeles",
                "Los Angeles HVAC Inspection"
            ]
        ]
    },
    {
        "cityName": "Sacramento",
        "keywords": [
            [
                "HVAC Company in Sacramento",
                "Sacramento HVAC Inspection"
            ]
        ]
    }
];}

Related