Implementing useMemo the right way - javascript

I am trying to understand the usage of useMemo. Since the object doesn't change, I thought I can improve performance by adding useMemo.
However as soon I add it here, I am being asked to add findFloorPosition. Once I did that my linter asks me to add useCallback to the findFloorPosition function.
Can you give me some advice what's the right way to implement useMemo here?
const location = useLocation();
const searchParams = parseQs(location.search);
const goto = searchParams?.goto;
const findFloorPosition = (elementId) => {
for (const floor of blueprint) {
for (const room of floor.rooms) {
const foundElement = room.elements.find(
(element) => element.id === elementId
);
if (foundElement) {
return floor.position;
}
}
}
};
const currentFloorPosition = useMemo(() => findFloorPosition(goto), [goto]);
Probably not relevant here, but here how the blueprint object looks like:
const blueprint = [
{
id: "4mD59WO",
name: "AUDITORIUM",
position: 1,
rooms: [
{
id: "zR8Qgpj",
name: "Audimax",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "1jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "1jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
},
{
id: "3mDd9WO",
name: "FOYER",
position: 0,
rooms: [
{
id: "4R8Qgpj",
name: "Speakers Table",
subtitle: null,
details: null,
position: 0,
elements: [
{
id: "2jLv04W",
position: 0,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
},
{
id: "2jLv12W",
position: 1,
type: "daily",
element: "listing_large",
properties: {
meetingId: null,
capacity: 6
}
}
]
}
]
}
];

Since functional components rely heavily on closures, its extremely important that when you memoize functions, you are using the correct and updated values from the closures.
The reason eslint warns you to add findFloorPosition to dependency is to make sure that nothing within findFloorPosition refers to old values
The above code can be implemented like
const findFloorPosition = useCallback((elementId) => {
for (const floor of blueprint) {
for (const room of floor.rooms) {
const foundElement = room.elements.find(
(element) => element.id === elementId
);
if (foundElement) {
return floor.position;
}
}
}
}, [blueprint]);
const currentFloorPosition = useMemo(() => findFloorPosition(goto), [goto, findFloorPosition]);

Notice that you should memoize a value only if the dependency array values (the value of goto) not frequently change.
The warning occurs because the linter (eslint) only evaluates the semantics, it doesn't know that findFloorPosition is just a helper function.
So basically you need to inline the helper function like:
const currentFloorPosition = useMemo(() => {
for (const floor of blueprint) {
for (const room of floor.rooms) {
const foundElement = room.elements.find(
(element) => element.id === goto
);
if (foundElement) {
return floor.position;
}
}
}
return null;
}, [goto]);
// Or
const currentFloorPosition = useMemo(() => {
const findFloorPosition = (elementId) => {
for (const floor of blueprint) {
for (const room of floor.rooms) {
const foundElement = room.elements.find(
(element) => element.id === elementId
);
if (foundElement) {
return floor.position;
}
}
}
};
return findFloorPosition(goto);
}, [goto]);

Related

How to sort a list of entities once per custom sort function in the right order?

Given a bunch of ( not sorted ) entities
const entities = [
{ id: "person-1", type: "person", fields: { age: 34 }},
{ id: "car-2", type: "car", fields: { manufacturer: "bar" }},
{ id: "house-2", type: "house", fields: { constructionYear: 2010 }},
{ id: "person-4", type: "person", fields: { age: 71 }},
{ id: "person-2", type: "person", fields: { age: 57 }},
{ id: "house-1", type: "house", fields: { constructionYear: 1968 }},
{ id: "car-1", type: "car", fields: { manufacturer: "foo" }},
{ id: "person-3", type: "person", fields: { age: 42 }},
];
and a bunch of "sources" with an optional sort object describing the sort index and a "isLessThan" compare function as a string
const sources = [
{ type: "person", sort: { index: 1, isLessThanFunctionAsString: "(left, right) => left.fields.age < right.fields.age" }},
{ type: "car" },
{ type: "house", sort: { index: 0, isLessThanFunctionAsString: "(left, right) => left.fields.constructionYear < right.fields.constructionYear" }},
];
Each source describes how to deal with entities of the given type. The source for "person" defines how entities of type "person" should be sorted.
I do not have any control over the configuration, the isLessThan function comes as a stringified function and its signature is (leftEntity: Entity, rightEntity: Entity) => boolean, so the logic inside the compare function could be anything
I want to sort the array entities by the information gathered from sources and started with
const entities = [{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}];
const sources = [{type:"person",sort:{index:1,isLessThanFunctionAsString:"(left, right) => left.fields.age < right.fields.age"}},{type:"car"},{type:"house",sort:{index:0,isLessThanFunctionAsString:"(left, right) => left.fields.constructionYear < right.fields.constructionYear"}}];
function sortEntities(unsortedEntities, allSources) {
// if there are no entities, there is nothing to do
if (unsortedEntities.length === 0) {
return unsortedEntities;
}
// only care for the sources with a sort function
const sourcesWithSort = allSources.filter(source => !!source.sort);
// if there are no sources with sort, there is nothing to do
if (sourcesWithSort.length === 0) {
return unsortedEntities;
}
// since we can only compare two entities of the same type we must sort the entities by type first
let sortedEntities = entities.sort((leftEntity, rightEntity) => {
// no need for sorting if both have the same type
if (leftEntity.type === rightEntity.type) {
return 0;
}
if (leftEntity.type < rightEntity.type) {
return -1;
}
return 1;
});
// we must sort the sources by sort index ( at this point we now that sort must exist )
const sortSources = sourcesWithSort.sort((leftSource, rightSource) => leftSource.sort.index - rightSource.sort.index);
// NOW we can start sorting the entities
for (const source of sortSources) {
sortedEntities = sortedEntities.sort((leftEntity, rightEntity) => {
const {
type
} = source;
// we can't compare entities if the types aren't equal to the source type
if (leftEntity.type !== type || rightEntity.type !== type) {
return 0;
}
const isLessThanFunction = (new Function("return " + source.sort.isLessThanFunctionAsString))();
const isLeftEntityLessThanRightEntity = isLessThanFunction(
leftEntity,
rightEntity
);
if (isLeftEntityLessThanRightEntity) {
return -1;
}
return 1;
});
}
return sortedEntities;
}
console.log(sortEntities([...entities], [...sources]));
My approach is getting really slow when dealing with many entities ( > 100 ) and many sources ( > 20 )
Do you have any ideas how to improve the code or maybe come up with faster alternatives?
You can change the columns array to use a comparator function which directly returns a sort order expected buy the sort comapreFn callback function. For the numerical fields it will be subtraction.
const columns = [
{ type: "person", sort: { index: 1, comparator: (left, right) => left.fields.age - right.fields.age }},
{ type: "car", sort: undefined },
{ type: "house", sort: { index: 0, comparator: (left, right) => left.fields.constructionYear - right.fields.constructionYear }},
];
If you want one for car, it would be
comparator: (a, b) => a.fields.manufacturer.localeCompare(b.fields.manufacturer)
Then you create a mapper object which maps the type to the index and the comparator function. If an index is not mentioned, then it is set to -Infinity (Because, in your case you have the car results on top)
const columnMap = columns.reduce((acc, o) => {
const { type, sort: { index = -Infinity, comparator } = {} } = o
acc[type] = { index, comparator };
return acc
}, {})
It creates this object:
{
"person": {
"index": 1,
"comparator": (a,b)=>a.fields.age-b.fields.age
},
"car": {
"index": -Infinity,
"comparator": undefined
},
"house": {
"index": 0,
"comparator": (a,b)=>a.fields.constructionYear-b.fields.constructionYear
}
}
Then sort the entities based on index value
if the index values are same (or both are -Infinity), then sort based on the type name
if both types are same, then sort based on the type specific comparator function.
Here's a working snippet:
const entities=[{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}],
columns=[{type:"person",sort:{index:1,comparator:(e,s)=>e.fields.age-s.fields.age}},{type:"car",sort:void 0},{type:"house",sort:{index:0,comparator:(e,s)=>e.fields.constructionYear-s.fields.constructionYear}}];
function sortEntities(array, columns) {
const columnMap = columns.reduce((acc, o) => {
const { type, sort: { index = -Infinity, comparator } = {} } = o
acc[type] = { index, comparator };
return acc
}, {})
return array.sort((a,b) =>
columnMap[a.type].index - columnMap[b.type].index
|| a.type.localeCompare(b.type)
|| columnMap[a.type].comparator?.(a,b)
)
}
console.log(sortEntities(entities, columns))
If you can't change the columns array, you can create your own comparator function by using eval to create a function using the string.
const entities=[{id:"person-1",type:"person",fields:{age:34}},{id:"car-2",type:"car",fields:{manufacturer:"bar"}},{id:"house-2",type:"house",fields:{constructionYear:2010}},{id:"person-4",type:"person",fields:{age:71}},{id:"person-2",type:"person",fields:{age:57}},{id:"house-1",type:"house",fields:{constructionYear:1968}},{id:"car-1",type:"car",fields:{manufacturer:"foo"}},{id:"person-3",type:"person",fields:{age:42}}],
columns =[{type:"person",sort:{index:1,isLessThanFunctionAsString:"(left, right) => left.fields.age < right.fields.age"}},{type:"car"},{type:"house",sort:{index:0,isLessThanFunctionAsString:"(left, right) => left.fields.constructionYear < right.fields.constructionYear"}}];
function sortEntities(array, columns) {
const columnMap = columns.reduce((acc, { type, sort }) => {
let index = -Infinity, comparator;
if (sort) {
eval("var isLessThanFunction =" + sort.isLessThanFunctionAsString)
index = sort.index;
comparator = (a, b) => isLessThanFunction(a, b) ? -1 : 1
}
acc[type] = { index, comparator };
return acc
}, {})
return array.sort((a, b) =>
columnMap[a.type].index - columnMap[b.type].index
|| a.type.localeCompare(b.type)
|| columnMap[a.type].comparator?.(a, b)
)
}
console.log(sortEntities(entities, columns))
First of all, you will need to make sure your data is prepared to go into a table. You will need to process the raw data and convert it into an array of records. After that, you can sort your data much easier. Lastly, you can convert the sorted records into a table and add it to the document.
Also, you have a key on your sorter called isLessThan. You should change that to something more generic like fn or callback. You will want all sorters to be keyed similarly.
const main = () => {
document.body.append(table(sort(process(entities), columns), columns));
};
const sort = (data, columns) => {
const sorters = columns
.map(({ sort }) => sort)
.filter(s => s)
.sort(({ index: a }, { index: b }) => a - b)
.map(({ isLessThan }) => isLessThan); // This is not good...
return data.sort((a, b) => {
let sorted = 0, index = 0;
while (sorted === 0 && index < sorters.length) {
sorted = sorters[index](a, b);
index++;
}
return sorted;
});
};
const process = (rawData) =>
Object.entries(rawData.reduce((acc, { id, type, fields }) => {
const [, index] = id.match(/\w+-(\d+)/);
if (!acc[index]) acc[index] = { id: +index };
acc[index][type] = { fields };
return acc;
}, {})).sort(([k1], [k2]) => k1 - k2).map(([, v]) => v);
const table = (records, columns) =>
group('table', {},
group('thead', {},
group('tr', {},
...columns.map(col =>
leaf('th', { innerText: col.type })
)
)
),
group('tbody', {},
...records.map(record =>
group('tr', {},
...columns.map(col =>
leaf('td', {
innerText: render(record[col.type], record, col)
})
)
)
)
)
);
const render = (data, record, column) =>
data ? JSON.stringify(data) : '';
const leaf = (tagName, options) =>
Object.assign(document.createElement(tagName), options);
const group = (tagName, options, ...children) =>
appendAll(leaf(tagName, options), children);
const appendAll = (el, children = []) => {
children.forEach(child => el.appendChild(child));
return el;
};
const entities = [
{ id: "person-1", type: "person", fields: { age: 34 }},
{ id: "car-2", type: "car", fields: { manufacturer: "bar" }},
{ id: "house-2", type: "house", fields: { constructionYear: 2010 }},
{ id: "person-4", type: "person", fields: { age: 71 }},
{ id: "person-2", type: "person", fields: { age: 57 }},
{ id: "house-1", type: "house", fields: { constructionYear: 1968 }},
{ id: "car-1", type: "car", fields: { manufacturer: "foo" }},
{ id: "person-3", type: "person", fields: { age: 42 }},
];
const columns = [
{ type: "id" },
{ type: "person", sort: { index: 1, isLessThan: (left, right) => left?.fields?.age < right?.fields?.age }},
{ type: "car" },
{ type: "house", sort: { index: 0, isLessThan: (left, right) => left?.fields?.constructionYear < right?.fields?.constructionYear }},
];
main();
table { border-collapse: collapse; }
table, th, td { border: thin solid grey; }
th, td { padding: 0.5rem; }
th { background: #DDD; }
tbody tr:nth-child(even) { background: #EEE; }
td { font-family: monospace; font-size: smaller; }

How to update deeply nested array of objects?

I have the following nested array of objects:
const data = [
{
product: {
id: "U2NlbmFyaW9Qcm9swkdWN0OjEsz",
currentValue: 34300,
},
task: {
id: "R2VuZXJpY1Byb2R1Y3Q6MTA",
name: "My Annuity",
},
instrumentDetails: [
{
instrument: {
id: "U2NlbmFyaW9JbnN0cnVtZW50OjEz",
supplier: {
id: "U3VwcGxpZXJJbnN0cnVtZW50OjUzNjQ",
supplierDetails: {
name: "Local - Class A",
},
},
currentValue: 44323,
},
assets: {
current: 1.2999270432626702,
fixed: 0.5144729302004819,
financial: 0.0723506386331588,
cash: 0.00006003594786398524,
alternative: 0.05214078143244779,
property: 0.548494862567579,
local: 0.10089348539739094,
global: 0,
},
},
],
},
{
product: {
id: "U2NlbmFyaW9Qcm9swkfefewdWN0OjEsz",
currentValue: 3435300,
},
task: {
id: "R2VuZXJpYfewfew1Byb2R1Y3Q6MTA",
name: "Living",
},
instrumentDetails: [
{
instrument: {
id: "U2NlbmFyadewwW9JbnN0cnVtZW50OjEz",
supplier: {
id: "U3VwcGxpZdwdwXJJbnN0cnVtZW50OjUzNjQ",
supplierDetails: {
name: "Local - Class B",
},
},
currentValue: 434323,
},
assets: {
current: 1.294353242,
fixed: 0.514434242004819,
financial: 0.07434286331588,
cash: 0.0000434398524,
alternative: 0.05242348143244779,
property: 0.543242567579,
local: 0.100432439739094,
global: 0,
},
},
],
},
];
The above data presents an array of products which consist of instruments that are described in instrumentDetails array. I am trying to find an instrument by supplier id and update its assets by multiplying all of the asset values by a given number.
Here is my function:
export const updateObject = (
productsArr: any,
supplierInstrumentId: string
) => {
return productsArr.map(
(product: any) => {
product.instrumentDetails.map(
(instrumentDetail: any) => {
if (
instrumentDetail.instrument.supplier.id ===
supplierInstrumentId
) {
instrumentDetail.assets.current = instrumentDetail.assets.current + 5;
instrumentDetail.assets.fixed= instrumentDetail.assets.fixed+ 5;
instrumentDetail.assets.financial= instrumentDetail.assets.financial+ 5;
instrumentDetail.assets.cash= instrumentDetail.assets.cash+ 5;
}
}
);
}
);
};
This function is giving an error :
Uncaught TypeError: Cannot assign to read only property 'current' of
object '#'
How can I deeply update the above data? Please help.
You need to return a new instrumentDetail-type object from the map function. Don't try to update the existing object.
(instrumentDetail: any) => {
const assets = instrumentDetail.instrument.supplier.id === supplierInstrumentId
? Object.fromEntries(
Object.entries(instrumentDetail.assets).map(([k, v]) => [k, v + 5])
)
:
instrumentDetail.assets;
return {
...instrumentDetail,
assets
};
}
Your product map is not returning which is why you're likely getting an undefined. I wasn't getting the typescript error which you mentioned above. This should leave the array in the state at which you intended.
const updateObject = (
productsArr: any,
supplierInstrumentId: string
) => {
return productsArr.map(
(product: any) => {
product.instrumentDetails.map(
(instrumentDetail: any) => {
if (
instrumentDetail.instrument.supplier.id ===
supplierInstrumentId
) {
instrumentDetail.assets.current += 5;
instrumentDetail.assets.fixed= instrumentDetail.assets.fixed+ 5;
instrumentDetail.assets.financial= instrumentDetail.assets.financial+ 5;
instrumentDetail.assets.cash= instrumentDetail.assets.cash+ 5;
return instrumentDetail;
}
}
);
return product;
}
);
};

Creating nested array from flat array - Data Structure

I need to create a nested array using the path as reference for the children.
E.g: 4.1 is a child of 4, 4.1.1 is a child of 4.1, 4.2 is a child of 4...
I have this flat array, with all the data and paths. How would be the best approach to create a nested array where the children are nested to its parent based on its path.
Input:
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
Output:
const list = [
{
location: 1,
path: '4',
children: [
{
location: 2,
path: '4.1',
children: [
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
]
},
{
location: 5,
path: '4.2',
children: [
{
location: 6,
path: '4.2.1'
},
]
},
{
location: 7,
path: '4.3',
children: [
{
location: 8,
path: '4.3.1'
}
]
},
]
},
];
The best approach would be something recursive.
Any suggestions for this algorithm?
I was curious if the linked answer from Scott would be able to solve this problem without modification. It does!
import { tree } from './Tree'
import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const myTree =
tree // <- make tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
[
{
"location": 1,
"path": "4",
"children": [
{
"location": 2,
"path": "4.1",
"children": [
{
"location": 3,
"path": "4.1.1",
"children": []
},
{
"location": 4,
"path": "4.1.2",
"children": []
}
]
},
{
"location": 5,
"path": "4.2",
"children": [
{
"location": 6,
"path": "4.2.1",
"children": []
}
]
},
{
"location": 7,
"path": "4.3",
"children": [
{
"location": 8,
"path": "4.3.1",
"children": []
}
]
}
]
}
]
The Tree module is shared in this post and here's a peek at the Func module that supplies bind -
// Func.js
const identity = x => x
const bind = (f, ...args) =>
f(...args)
const raise = (msg = "") => // functional throw
{ throw Error(msg) }
// ...
export { identity, bind, raise, ... }
Expand the snippet below to verify the results in your browser -
// Func.js
const bind = (f, ...args) =>
f(...args)
// Index.js
const empty = _ =>
new Map
const update = (r, k, t) =>
r.set(k, t(r.get(k)))
const append = (r, k, v) =>
update(r, k, (all = []) => [...all, v])
const index = (all = [], indexer) =>
all.reduce
( (r, v) => append(r, indexer(v), v)
, empty()
)
// Tree.js
// import { index } from './Index'
function tree (all, indexer, maker, root = null)
{ const cache =
index(all, indexer)
const many = (all = []) =>
all.map(x => one(x))
const one = (single) =>
maker(single, next => many(cache.get(next)))
return many(cache.get(root))
}
// Main.js
// import { tree } from './Tree'
// import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const list =
[{location:1,path:'4'},{location:2,path:'4.1'},{location:3,path:'4.1.1'},{location:4,path:'4.1.2'},{location:5,path:'4.2'},{location:6,path:'4.2.1'},{location:7,path:'4.3'},{location:8,path:'4.3.1'}]
const myTree =
tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
One way to do this is to use an intermediate index mapping paths to objects, then folding your list into a structure by looking up each node and its parent in the index. If there is no parent, then we add it to the root object. In the end, we return the children of our root object. Here's some code for that:
const restructure = (list) => {
const index = list .reduce(
(a, {path, ...rest}) => ({...a, [path]: {path, ...rest}}),
{}
)
return list .reduce((root, {path}) => {
const node = index [path]
const parent = index [path .split('.') .slice(0, -1) .join('.')] || root
parent.children = [...(parent.children || []), node]
return root
}, {children: []}) .children
}
const list = [{location: 1, path: '4'}, {location: 2, path: '4.1' }, {location: 3, path: '4.1.1'}, {location: 4, path: '4.1.2'}, {location: 5, path: '4.2'}, {location: 6, path: '4.2.1'}, {location: 7, path: '4.3'}, {location: 8, path: '4.3.1'}]
console.log (restructure (list))
.as-console-wrapper {min-height: 100% !important; top: 0}
Using the index means that we don't have to sort anything; the input can be in any order.
Finding the parent involves replacing, for instance, "4.3.1" with "4.3" and looking that up in the index. And when we try "4", it looks up the empty string, doesn't find it and uses the root node.
If you prefer regex, you could use this slightly shorter line instead:
const parent = index [path.replace (/(^|\.)[^.]+$/, '')] || root
But, you might also want to look at a more elegant technique in a recent answer on a similar question. My answer here, gets the job done (with a bit of ugly mutation) but that answer will teach you a lot about effective software development.
You can first sort the array of objects by path so that the parent will always be before it's children in the sorted array.
eg: '4' will be before '4.1'
Now, you can create an object where the keys are the paths. Let's assume '4' is already inserted in our object.
obj = {
'4': {
"location": 1,
"path": "4",
}
}
When we process '4.1', we first check if '4' is present in our object. If yes, we now go into its children (if the key 'children' isn't present, we create a new empty object) and check if '4.1' is present. If not, we insert '4.1'
obj = {
'4': {
"location": 1,
"path": "4",
"children": {
"4.1": {
"location": 2,
"path": "4.1"
}
}
}
}
We repeat this process for each element in list. Finally, we just have to recursively convert this object into an array of objects.
Final code:
list.sort(function(a, b) {
return a.path - b.path;
})
let obj = {}
list.forEach(x => {
let cur = obj;
for (let i = 0; i < x.path.length; i += 2) {
console.log(x.path.substring(0, i + 1))
if (x.path.substring(0, i + 1) in cur) {
cur = cur[x.path.substring(0, i + 1)]
if (!('children' in cur)) {
cur['children'] = {}
}
cur = cur['children']
} else {
break;
}
}
cur[x.path] = x;
})
function recurse (obj) {
let res = [];
Object.keys(obj).forEach((key) => {
if (obj[key]['children'] !== null && typeof obj[key]['children'] === 'object') {
obj[key]['children'] = recurse(obj[key]['children'])
}
res.push(obj[key])
})
return res;
}
console.log(recurse(obj));
was thinking along the same terms as Aadith but came up from an iterative approach. I think the most performant way to do it is to use a linked list structure and then flatten it though.
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
let newList = [];
list.forEach((location) =>
{
console.log('Handling location ',location);
if(location.path.split('.').length==1)
{
location.children = [];
newList.push(location);
}
else
{
newList.forEach(loc => {
console.log('checking out: ',loc);
let found = false;
while(!found)
{
console.log(loc.path,'==',location.path.substring(0, location.path.lastIndexOf('.')));
found = loc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(!found)
{
for(let i=0;i<loc.children.length;i++)
{
let aloc = loc.children[i];
found = aloc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(found)
{
console.log('found it...', loc);
location.children = [];
aloc.children.push(location);
break;
}
}
}
else
{
console.log('found it...', loc);
location.children = [];
loc.children.push(location);
}
}
} );
}
});
console.log(newList);
This was my quick and dirty way of how to go about it
Thank you for all the suggestions!
I could definitely see really sophisticated solutions to my problem.
By the end of the day, I've ended up creating my own "dirty" solution.
It is definitely a slower approach, but for my application this list won't be long to the point where i should be too worry about optimization.
I had simplified the flatted list for the purpose of my question. Although, in reality the list was a little more complex then that. I could also pass the path I want to find its children.
This is the solution that worked for me.
function handleNested(list, location) {
return list.map((item) => {
if (item.children && item.children.length > 0) {
item.children = handleNested(item.children, location);
}
if (
location.path.startsWith(item.path) &&
location.path.split(".").length === item.path.split(".").length + 1
) {
return {
...item,
children: item.children ? [...item.children, location] : [location],
};
} else {
return item;
}
});
}
function locationList(path) {
// Filtering the list to remove items with different parent path, and sorting by path depthness.
const filteredList = list
.filter((location) => location.path.startsWith(path))
.sort((a, b) => a.path.length - b.path.length);
let nestedList = [];
// Looping through the filtered list and placing each item under its parent.
for (let i = 0; i < filteredList.length; i++) {
// Using a recursive function to find its parent.
let res = handleNested(nestedList, filteredList[i]);
nestedList = res;
}
return nestedList;
}
locationList("4");

Appending to an array access by a key in Redux Reducer

I'm trying to add to my state within a reducer. I need to access a key, and then add to an array within that key. This is what my state currently looks like:
{teamMembers: {
PersonId1: [{ object1 }, {object2 }]
PersonId2: [{ object3 }, { object4 }]
PersonId3: [{ object5 }, { object6 }]
}}
I need to access a PersonId, based on what the action inputs, and then append an item from the action to the array. Ideally it would also create a new key and array if the key PersonId didn't already exist.
In your reducer, just do a little data manipulation. Remember to only manipulate a copy of your state and not your actual state.
const action = {
payload: {
personId: 'PersonId4',
item: {}
}
}
const state = {
PersonId1: [{ object1 }, {object2 }]
PersonId2: [{ object3 }, { object4 }]
PersonId3: [{ object5 }, { object6 }]
}
const {personId, item} = action.payload
let newState = {...state}
if (personId in newState) {
newState[personId].push(item)
} else {
newState[personId] = [item]
}
return newState
You can conditionally check for the existence of the key and then add it to an already existing array or create a new one.
Something like this:
const newItems = state[id] ? [...state[id], ...items] : items;
//...
return {
...state,
[id]: newItems
}
Note that i'm using the object spread syntax which is a proposal (in stage 3) and you need the babel plugin babel-plugin-transform-object-rest-spread to support it (or babel stage 3 preset).
Running example:
const initialState = {
teamMembers: {
PersonId1: [{
name: 'object1'
}, {
name: 'object2'
}],
PersonId2: [{
name: 'object3'
}, {
name: 'object4'
}],
PersonId3: [{
name: 'object5'
}, {
name: 'object6'
}]
}
};
const actionOne = {
type: 'ADD',
payload: {
id: 'PersonId2',
items: [{
name: 'object444'
}]
}
}
const actionTwo = {
type: 'ADD',
payload: {
id: 'PersonId777',
items: [{
name: 'object777'
}]
}
}
const reducer = (state = {}, action) => {
switch (action.type) {
case 'ADD':
{
const {
id,
items
} = action.payload;
const newItems = state[id] ? [...state[id], ...items] : items;
return {
...state,
[id]: newItems
}
}
default:
return state;
}
}
const reducerResultOne = reducer(initialState.teamMembers, actionOne);
console.log('reducerResultOne', reducerResultOne);
const reducerResultTwo = reducer(initialState.teamMembers, actionTwo);
console.log('reducerResultTwo', reducerResultTwo);

Find value in javascript array of objects deeply nested with ES6

In an array of objects I need to find a value -- where key is activity : However the activity key can be deeply nested in the array like so:
const activityItems = [
{
name: 'Sunday',
items: [
{
name: 'Gym',
activity: 'weights',
},
],
},
{
name: 'Monday',
items: [
{
name: 'Track',
activity: 'race',
},
{
name: 'Work',
activity: 'meeting',
},
{
name: 'Swim',
items: [
{
name: 'Beach',
activity: 'scuba diving',
},
{
name: 'Pool',
activity: 'back stroke',
},
],
},
],
},
{} ...
{} ...
];
So I wrote a recursive algorithm to find out if a certain activity is in the array:
let match = false;
const findMatchRecursion = (activity, activityItems) => {
for (let i = 0; i < activityItems.length; i += 1) {
if (activityItems[i].activity === activity) {
match = true;
break;
}
if (activityItems[i].items) {
findMatchRecursion(activity, activityItems[i].items);
}
}
return match;
};
Is there an ES6 way of determining if an activity exists in an array like this?
I tried something like this:
const findMatch(activity, activityItems) {
let obj = activityItems.find(o => o.items.activity === activity);
return obj;
}
But this won't work with deeply nested activities.
Thanks
You can use some() method and recursion to find if activity exists on any level and return true/false as result.
const activityItems = [{"name":"Sunday","items":[{"name":"Gym","activity":"weights"}]},{"name":"Monday","items":[{"name":"Track","activity":"race"},{"name":"Work","activity":"meeting"},{"name":"Swim","items":[{"name":"Beach","activity":"scuba diving"},{"name":"Pool","activity":"back stroke"}]}]}]
let findDeep = function(data, activity) {
return data.some(function(e) {
if(e.activity == activity) return true;
else if(e.items) return findDeep(e.items, activity)
})
}
console.log(findDeep(activityItems, 'scuba diving'))
While not as elegant as a recursive algorithm, you could JSON.stringify() the array, which gives this:
[{"name":"Sunday","items":[{"name":"Gym","activity":"weights"}]},{"name":"Monday","items":[{"name":"Track","activity":"race"},{"name":"Work","activity":"meeting"},{"name":"Swim","items":[{"name":"Beach","activity":"scuba diving"},{"name":"Pool","activity":"back stroke"}]}]}]
You could then use a template literal to search for the pattern:
`"activity":"${activity}"`
Complete function:
findMatch = (activity, activityItems) =>
JSON.stringify(activityItems).includes(`"activity":"${activity}"`);
const activityItems = [{
name: 'Sunday',
items: [{
name: 'Gym',
activity: 'weights',
}, ],
},
{
name: 'Monday',
items: [{
name: 'Track',
activity: 'race',
},
{
name: 'Work',
activity: 'meeting',
},
{
name: 'Swim',
items: [{
name: 'Beach',
activity: 'scuba diving',
},
{
name: 'Pool',
activity: 'back stroke',
},
],
},
],
}
];
findMatch = (activity, activityItems) =>
JSON.stringify(activityItems).includes(`"activity":"${activity}"`);
console.log(findMatch('scuba diving', activityItems)); //true
console.log(findMatch('dumpster diving', activityItems)); //false
First, your function could be improved by halting once a match is found via the recursive call. Also, you're both declaring match outside, as well as returning it. Probably better to just return.
const findMatchRecursion = (activity, activityItems) => {
for (let i = 0; i < activityItems.length; i += 1) {
if (activityItems[i].activity === activity) {
return true;
}
if (activityItems[i].items && findMatchRecursion(activity, activityItems[i].items) {
return true;
}
}
return false;
};
There's no built in deep search, but you can use .find with a named function if you wish.
var result = !!activityItems.find(function fn(item) {
return item.activity === "Gym" || (item.items && item.items.find(fn));
});
We now use object-scan for simple data processing tasks like this. It's really good once you wrap your head around how to use it. Here is how one could answer your questions
// const objectScan = require('object-scan');
const find = (activity, input) => objectScan(['**'], {
abort: true,
rtn: 'value',
filterFn: ({ value }) => value.activity === activity
})(input);
const activityItems = [{"name":"Sunday","items":[{"name":"Gym","activity":"weights"}]},{"name":"Monday","items":[{"name":"Track","activity":"race"},{"name":"Work","activity":"meeting"},{"name":"Swim","items":[{"name":"Beach","activity":"scuba diving"},{"name":"Pool","activity":"back stroke"}]}]}]
console.log(find('scuba diving', activityItems));
// => { name: 'Beach', activity: 'scuba diving' }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

Categories

Resources