cezarneaga
9/15/2017 - 2:35 PM

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'))(data);

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'})}))(data);

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})}))(data);

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', data[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.