0

I have a React component that I'm using a dictionary inside of to compare against an API response for address state, and map only the states that are returned back as options in a dropdown.

This is the mapping function I'm using to create the array of returned states:

let activeStates = props.locations.map(x => ({abbr: x.address.state, name: states[x.address.state]as string}));

Against my dictionary which looks like this:

const states = {
  AL: "Alabama",
  AK: "Alaska",
  AZ: "Arizona",
  AR: "Arkansas",
  CA: "California",
  CO: "Colorado",
  (all US states are included)
};

And that creates an array of all the location addresses in a response like this:

3:
abbr: "3"
name: {abbr: "TN", name: "Tennessee"}
__proto__: Object
4:
abbr: "4"
name: {abbr: "WI", name: "Wisconsin"}
__proto__: Object
5:
abbr: "5"
name: {abbr: "NC", name: "North Carolina"}
__proto__: Object
6:
abbr: "6"
name: {abbr: "NC", name: "North Carolina"}

That feeds my Component where I'm mapping the array to the dropdown:

<select value={state} onChange={handleStateChange}>
  <option value={''}>All</option>
    {activeStates.map((state) => (
  <option value={state.abbr}>{state.name}</option>))}
</select>

I'm trying to write a function that works off of activeStates to create a new array of only one of each state/abbreviation instead of producing duplicates. I know that because all of the values returned in that mapped array are considered unique I need to run an indexing function to get rid of the duplicates, but I'm not sure how.

So far I've used:

let statesObj = props.locations.reduce((acc, curr) => {
   acc[curr.address.state] = states[curr.address.state];
   return acc;
}, {});


  let uniqueStates = Object.entries(statesObj).map(([abbr, name]) => ({abbr, name }));
  let activeStates = props.locations.map(x => ({abbr: x.address.state, name: states[x.address.state]}));
  let uniqueStates = new Set(activeStates);
  let uniqueStatesArray = [...uniqueStates]

and

let activeStates = Array.from(new Set(props.locations.map(x => ({ abbr: x.address.state, name: states[x.address.state]})))

None of which have worked.

Josh
  • 1,165
  • 3
  • 24
  • 44
  • 1
    Look into using Lodash's uniqBy functionality, it will save you a lot of time and wasted energy...usage would be like the following: _.uniqBy(activeStates, 'abbrv') https://lodash.com/docs/4.17.15#uniqBy – MattE Feb 07 '20 at 16:34
  • It seems like you might have better luck by just not introducing the duplicates in the first place. Where are the duplicates coming from? – Heretic Monkey Feb 07 '20 at 16:37
  • @MattE I tried bringing lodash in but I'm getting a type error for the underscore in ```_.uniqBy``` do you know if there's a typescript types dependency for that? – Josh Feb 07 '20 at 18:12
  • 1
    @Josh in typescript you need to import it. import _ as * from 'lodash'; – MattE Feb 07 '20 at 18:23
  • @MattE did that and added the logic ```let uniqueStates = _.uniqBy(activeStates, 'abbr')``` and now I'm getting an error stating ```TypeError: Cannot read property 'uniqBy' of undefined``` – Josh Feb 07 '20 at 18:42
  • 1
    @Josh You might have to play around with it a little bit due to the structure you are using, which is less than ideal if I am being honest – MattE Feb 07 '20 at 18:52
  • @MattE Yeah I actually ended up going another route and got it working. I'm playing alongside someone else's code so the process has been a tad frustrating haha. Thanks for the help! – Josh Feb 07 '20 at 19:04
  • @HereticMonkey the problem was that there are a bunch of location records and some of them share the same address state so we have to keep all of them while also trimming out the duplicate states for the filter so that only the unique states show up in the dropdown. Bit of a pain but hopefully we can nix that on the API side in the future haha – Josh Feb 07 '20 at 19:10

2 Answers2

1

You are on the right track.

When adding objects to a Set it is by reference:

const s = new Set()
const o = { a: 1, b: 2 }

s.add(o)

console.log(s.has(o)) // true
console.log(s.has({ a: 1, b: 2 })) // false

This can lead to other effects you might not anticipate:

const s = new Set()
s.add({ a: 1, b: 2 })
s.add({ a: 1, b: 2 })
s.add({ a: 1, b: 2 })

console.log(s) // Set { { a: 1, b: 2 }, { a: 1, b: 2 }, { a: 1, b: 2 } }

To answer your question, you can use one of the object's properties to add to a set you use for checking:

const dropDuplicates = arr => {
  const seen = new Set()
  const result = []

  for (let obj of arr) {
    if (!seen.has(obj.abbr)) { // check if `abbr` has been seen
      result.push(obj) // if not, then add state to result
      seen.add(obj.abbr) // this state is now seen, don't add anymore
    }
  }

  return result
}
Kevin
  • 401
  • 2
  • 9
  • I'm getting a type error for ```obj``` at the ```result.push``` line stating ```Argument of type 'any' is not assignable to parameter of type 'never'.ts(2345)``` – Josh Feb 07 '20 at 17:59
  • @Josh, I don't know much about typescript as I should but after looking around it looks like you need to define the array type of `results`. It defaults to never. [Link](https://stackoverflow.com/questions/52423842/what-is-not-assignable-to-parameter-of-type-never-error-in-typescript) – Kevin Feb 07 '20 at 20:03
0

I ended up working it out by chaining a few mapping functions together with a filter like:

  let activeStates = props.locations.map(x => ({abbr: x.address.state, name: states[x.address.state]as string}));

  function getUnique(activeStates, comp) {

    const unique = activeStates
      .map(e => e[comp])
      // store the keys of the unique states
      .map((e, i, final) => final.indexOf(e) === i && i)
      // eliminate the duplicate keys and store unique states
      .filter(e => activeStates[e]).map(e => activeStates[e]);
     return unique;
  }

  let uniqueStates = getUnique(activeStates, 'abbr')

A bit of a roundabout but it ended up working and now all the options are mapping correctly. Thanks everyone for the great suggestions!

Josh
  • 1,165
  • 3
  • 24
  • 44