61

In ES6 using find or filter I'm quite comfortable iterating through to find an element in an array using a value.

However, I'm trying to get a value from a parent array based upon a value from a nested array.

For example, in this data structure:

products: [
  {
    id: 01,
    items: [
      {
        id: 01,
        name: 'apple'
      },
      {
        id: 02,
        name: 'banana'
      },
      {
        id: 03,
        name: 'orange'
      }
    ]
  },
  {
    id: 02,
    items: [
      {
        id: 01,
        name: 'carrot'
      },
      {
        id: 02,
        name: 'lettuce'
      },
      {
        id: 03,
        name: 'peas'
      }
    ]
  },
  {
    id: 03,
    items: [
      {
        id: 01,
        name: 'eggs'
      },
      {
        id: 02,
        name: 'bread'
      },
      {
        id: 03,
        name: 'milk'
      }
    ]
  }
]

If I know the name or id of the object milk, is there a way to find out the id of the element it's nested within?

Currently I have this:

products.find((product) => {
  product.find((prod) => {
    return prod.name === 'milk';
  });
});

Which only returns the object containing milk.

1
  • I think this is a good question; a suggestion for improvement would be to make the products: example shorter to make it easier for the reader to scan through (and shorten the length of the question). Commented Feb 2, 2023 at 19:04

6 Answers 6

103

You have to return something from the callback of the outer find. In fact, for the inner iteration you shouldn't use find but rather some that returns a boolean for whether an element matching the condition exists within the arrray:

products.find((product) => {
  return product.items.some((item) => {
//^^^^^^
    return item.name === 'milk';
  });
});

or in short:

products.find(product => product.items.some(item => item.name === 'milk'));

Then check whether find found something (not null!) and get its .id, the result should be 03. Alternatively, you can filter for the products containing milk as an item and then map all the results to their id:

products.filter(product =>
  product.items.some(item => item.name === 'milk');
).map(product =>
  product.id
) // [03]
Sign up to request clarification or add additional context in comments.

5 Comments

That makes sense - I hadn't seen some before.. but using this, how would I return the id of the outer element? For example, for the item milk the containing element has an id of 3. Thanks!
just quick note, the some should happen on product.items, not on product directly
@Toby the code Bergi gave you will find your object and all you have to do is call .id on it: const yourId = products.find(product => product.items.some(item => item.name === 'milk')).id
what if i want to get array index?
@Kakajann Just use findIndex instead of find!
22

I know you mention ES6, but in this case (and if you want to return the inner object) I believe it's better using for/of instead of map/reduce/find:

for (let p of products) {
  for (let i of p.items) {
    if (i.name === 'milk') return i;
  }
}

1 Comment

This seems like the cleanest solution (for both readability and performance). This is one of the few cases where functional programming isn't a good fit for the problem.
18

Another approach:

products
  .map((category) => category.items)
  .flat()
  .find((product) => product.name === 'milk');

2 Comments

to make it more shorter, instead of map() & flat() just use flatMap()
Should be the accepted answer ;)
8

UPDATE

As Ricardo Marimon commented, reduce does not break so it keeps searching over the array, So with that in mind as I don't like to use for loops imperative way of programming, its possible to break early from a reduce by mutating the used array, but that would also be bad, so instead its possible to make a copy and mutate the copy instead too.

// slice creates a copy of products
return products.slice(0).reduce((prev, product, i, arr) => {
    console.log(i);
    const findItem = prev || product.items.find(item => item.name === 'milk');
    if (typeof findItem !== 'undefined') arr.splice(1); // ejects early
    return findItem;
}, undefined);

const products = [
  {id: 1, items: [
    {id: 1, name: 'apple'},
    {id: 2, name: 'banana'},
    {id: 3, name: 'orange'}
  ]},
  {id: 2, items: [
    {id: 1, name: 'carrot'},
    {id: 2, name: 'lettuce'},
    {id: 3, name: 'milk'}
  ]},
  {id: 3, items: [
    {id: 1, name: 'eggs'},
    {id: 2, name: 'bread'},
    {id: 3, name: 'peas'}
  ]}
];

const findItem = products.slice(0).reduce((prev, product, i, arr) => {
    console.log(i);
    const findItem = prev || product.items.find(item => item.name === 'milk');
    if (typeof findItem !== 'undefined') arr.splice(1); // ejects early
    return findItem;
}, undefined);

console.log(findItem);

OLD

The accepted answer didn't do it for me because I wanted the result of the inner find, using both it always gave me the result of the outer filter/find, and I had to use the resulting array to find the value again.

So instead I used reduce with short-circuit to get the inner result.

// undefined as the initial value is necessary, otherwise it gets the first value of the array instead.

return products.reduce((prev, product) => prev || product.items.find(item => item.name === 'milk'), undefined);

const products = [
  {id: 1, items: [
    {id: 1, name: 'apple'},
    {id: 2, name: 'banana'},
    {id: 3, name: 'orange'}
  ]},
  {id: 2, items: [
    {id: 1, name: 'carrot'},
    {id: 2, name: 'lettuce'},
    {id: 3, name: 'peas'}
  ]},
  {id: 3, items: [
    {id: 1, name: 'eggs'},
    {id: 2, name: 'bread'},
    {id: 3, name: 'milk'}
  ]}
];

console.log(products.reduce((prev, product) => prev || product.items.find(item => item.name === 'milk'), undefined));

3 Comments

Thanks, Gustavo! Been scouring the web for a solution to get the inner find. You're awesome!
Beware that this way continues to search for the item even after one is found. The reduce function can't break. The solution by @ariel does break upon finding.
Thanks Ricardo Marimon by pointing out that problem, I updated the answer to fix that by using two "hacky" ways of using reduce instead of for loops.
2

If there is not a convenient way to get the data you need from the datastructure you have, then start by changing the datastructure:

// find once
products
  .flatMap((category) => category.items)
  .find((product) => product.name === 'milk');

// find often
const productsByName = Object.fromEntries(
  products.flatMap(
    (category) => category.items.map(
      (item) => [item.name, item]
    )
  )
);
productsByName['milk']

flatMap is the same as doing a map first followed by a flat of 1 level (the default). Object.entries transform [['key', 'value']] into {key: 'value'}

1 Comment

I like the idea of using flatMap. Easy and Works perfect!
1

To get the item directly without doubling back to get the id/object:

const products = [
  { id: 01,
    items: [ { id: 01, name: 'apple' },
             { id: 02, name: 'banana'},
             { id: 03, name: 'orange'}]},
  { id: 02,
    items: [ { id: 01, name: 'carrot' },
             { id: 02, name: 'lettuce'},
             { id: 03, name: 'peas'   }]},
  { id: 03,
    items: [ { id: 01, name: 'eggs'  },
             { id: 02, name: 'bread' },
             { id: 03, name: 'milk'  }]}
 ]

    let found;
    for ( const category of products ){
        found = category.items.find( item => item.name == "milk" )
        if ( found ) break
    }
    console.log( found ) 
    // == { id: 3, name: 'milk' }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.