Skip to content

Instantly share code, notes, and snippets.

@cezarneaga
Last active April 26, 2023 07:52
Show Gist options
  • Save cezarneaga/e7377357d62a2b2909685c1fb94125bb to your computer and use it in GitHub Desktop.
Save cezarneaga/e7377357d62a2b2909685c1fb94125bb to your computer and use it in GitHub Desktop.
Filter array of objects by nested values using ramda: Sometimes you dont have access to backend and you want to filter the response from an endpoint based on certain criteria. While trivial on flat arrays, this gets a bit tricky if the property you want to query is deeply nested. This is where Ramda shines.

Say we have a prop.users of the shape:

const users = [
    {username: 'bob', age: 30, tags: [{name: 'work', id: 1}, {name: 'boring', id: 2}]},
    {username: 'jim', age: 25, tags: [{name: 'home', id: 3}, {name: 'fun', id: 4}]},
    {username: 'jane', age: 30, tags: [{name: 'vacation', id: 5}, {name: 'fun', id: 4}]}
];

if you want to filter by username or age it's quite strait forward:

R.filter(R.propEq('username', 'jane'))(users);

things get tricky when you have something like tags and you want to filter all users with name: 'fun'

TLDR

this is the solution:

const hasFunTag = R.any(R.propEq('name', 'fun'))
R.filter(R.compose(hasFunTag, R.prop('tags')))(users)

This was how i got there

my first approach to solve this was:

R.filter(R.where({tags: R.contains({name:'fun'})}))(users);

which didn't work because the tags array can contain other properties like id in our case. this is consistent with REST APIs responses. you could solve it by providing the id too:

R.filter(R.where({tags: R.contains({name:'fun', id: 4})}))(users);

but most times you only know name and if you want to write a function and provide the filter as argument, you are stuck.

brains tend to jump to solutions by resorting to what they already know. I knew i could solve my problem if i removed id from the array and that was something i knew how to do:

const changeTags = (tag) => R.project(['name'], tag)
const extract = users.map(user => ({...user, tags: changeTags(user.tags)}))
R.filter(R.where({tags: R.contains({name:'fun'})}))(extract);

Let's see what happens here:

  • R.project helps you pick the properties you need from the given tag object.
  • use spread operator and change tags based on function above.
  • now we can filter as intended.

It solves the querying issue i had but it also transforms the initial users array and if your code uses somehow the id in the tags array or you typed your tags array, you are stuck.

this led me to think the brain needed to learn something new. I asked ramda and FP master @asharif and my React Vienna mentor to help me out of my mental paradigm. I asked him if ramda can solve this without modifying the initial array. 15' later here comes the other way of thinking my brain needed.

const hasFunTag = R.any(R.propEq('name', 'fun'))
R.filter(R.compose(hasFunTag, R.prop('tags')))(users)

it seems like all we needed was a R.any and a R.compose in the mix. I'm gonna try an explanation:

  1. R.filter takes the users array and looks at each object inside.
  2. R.compose in ramda executes from right to left so
  3. We need the path inside the object where hasFunTag function should look. R.prop('tags', users[0]) lets say, returns [{"id": 1, "name": "work"}, {"id": 2, "name": "boring"}] .
  4. R.any(R.propEq('name', 'fun'))(of above [] result) returns either true or false depending on whether there is a name key with value fun. For the above array it returns false. R.any helps us out of the 'removing keys before checking' issue i talked about before.
  5. This response in turn becomes the filter predicate and removes the first object from the users array.

This opens up the possibility for further abstraction of course. I'd like to explore that in the future.

@kumarpatel
Copy link

@cezarneaga

const data = [
  {
    "title": "Mar 23, 2018",
    "data": [
      {
        "away_team": "Utah",
        "home_team": "San Antonio",
      },
      {
        "away_team": "Boston",
        "home_team": "Portland",
      },
      {
        "away_team": "Atlanta",
        "home_team": "Golden State",
      }
    ]
  },
  {
    "title": "Mar 24, 2018",
    "data": [
      {
        "away_team": "Minnesota",
        "home_team": "Philadelphia",
      },
      {
        "away_team": "Chicago",
        "home_team": "Detroit",
      },
      {
        "away_team": "Boston",
        "home_team": "Detroit",
      }
    ]
  }
];
const filterbyAwayTeam = R.any(R.propEq('away_team', 'Boston'))
R.filter(R.compose(filterbyAwayTeam, R.prop('data')))(data)

For the above data structure, which is very similar to the structure described in your gist, how would I get the below result for term "Boston" on "away_team" field?

i.e

[
  {
    "title": "Mar 23, 2018",
    "data": [
      {
        "away_team": "Boston",
        "home_team": "Portland",
      }
    ]
  },
  {
    "title": "Mar 24, 2018",
    "data": [
      {
        "away_team": "Boston",
        "home_team": "Detroit",
      }
    ]
  }
]

@cezarneaga
Copy link
Author

sorry @kumarpatel i just saw your comment now. didn't get a notification. is the question still actual? pls tag me in comment

@lsloan
Copy link

lsloan commented Oct 7, 2019

A few places in your examples you refer to data, but your data is actually in the variable users. I think you made some typos and meant to refer to users.

@cezarneaga
Copy link
Author

A few places in your examples you refer to data, but your data is actually in the variable users. I think you made some typos and meant to refer to users.

you are right. meant users. thanks @lsloan ! fixed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment