#Ngrx/store: how to query models with relationships - javascript

I an Angular 2 app using Redux (with #ngrx/store), I have modeled the store this way:
{
modelA: {
ids: [1, 2],
entities: { 1: { name: "name modelA 1" },
2: { name: "name modelA 2" }
}
},
modelB: {
ids: [5, 8],
entities: { 5: { name: "name modelB 5" },
8: { name: "name modelA 8" },
9: { name: "name modelA 9" }
}
}
}
Basically, I have 2 types of objects: modelA and modelB. This is ok for now.
But I can't find which is the best way to write a relationship between then, representing something like modelA has many modelB (one-to-many). Can I do something like this?
modelAmodelB: {
entities: {
1: [5],
2: [8, 9]
}
}
This is in the root of the store, it's not a child from 'modelA'.
This might work, but how then would I 'query' the modelB from a specific modelA, using #ngrx/store methods? Because if I write a selector function that reads the global state and returns the partial state from modelAmodelB, I don't have access to the rest of the state when I compose my functions. Example:
compose(getModelAEntities(number[]), getModelAModelBRelations(modelA_id: number), getModelAModelBState() );
I can query this using Observable.combineLast
Observable
.combineLatest(
this.store.select('contentContents'),
this.store.select('contents'),
(relations: any, contents: any) => {
return relations.entities[1].map( id => {
return contents.entities[id]
})
}
).subscribe( data => {
console.log(data);
})
But I don't know if this is right: anytime I change modelA entities object (adding a new one, for example), the subscribe() is called, but the output is the same, because neither modelA entity has changed nor its modelB related objects.
PS: I could do the query this way
export const getModelAModelBs = (id) => {
return state =>
state.select( (s: any) => [s.modelAModelB.entities[id], s.modelB.entities])
.distinctUntilChanged( (prev, next) => {
const new_m = (next[0] || []).map( id => {
return next[1][id];
});
const old_m = (next[0] || []).map( id => {
return prev[1][id];
});
return prev[0] === next[0] && (JSON.stringify(new_m) === JSON.stringify(old_m))
})
.map( ([ids = [], modelBs]) => ids.map( id => modelBs[id]) );
};
//use the selector
this.store.let(getModelAModelBs(1)).subscribe( data => {
console.log(data)
})
But I don't know if this is the best approach.

I was struggling with the same issue and came up with a solution of wrappers.
Please take a look at the ngrx-entity-relationship library: https://www.npmjs.com/package/ngrx-entity-relationship
you can create selectors like that:
export const selectUserWithCompany = entityUser(
entityUserCompany(),
);
and then to use it with the store
this.store.select(selectUserWithCompany, 'userId');
to get
{
id: 'userId',
companyId: 'companyId',
company: {
id: 'companyId',
// ...,
},
// ...,
}

Related

saveData method saves twice

I am building a React app that includes one separate component for CRUD functionality of Products and another separate component for CRUD functionality of Suppliers.
I am using the same saveData method for both components (the Create functionality of CRUD.. that is triggered when the User presses Save after filling in the input fields of Product or Supplier). The saveData method is located in a central ProductsAndSuppliers.js file that is available to both the Products and Supplier components.
In both of the Product & Supplier components, there is a table showing the Products or Suppliers already present as dummy data.
I made a button at the bottom of each page to add a new Product or Supplier... depending on which tab the user has selected on the left side of the screen (Product or Supplier).
Since I am using the same saveData method in both cases, I have the same problem whenever I try to add a new Product or Supplier to each respective table after filling out the input fields. My new Product or Supplier is added.. but twice and I can't figure out why.
I have tried using a spread operator to add the new item to the collection but am having no success:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState((collection) => {
return { ...collection, item }
})
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
Here is my original saveData method that adds the new Product or Supplier, but twice:
saveData = (collection, item) => {
if (item.id === "") {
item.id = this.idCounter++;
this.setState(state => state[collection]
= state[collection].concat(item));
} else {
this.setState(state => state[collection]
= state[collection].map(stored =>
stored.id === item.id ? item : stored))
}
}
my state looks like this:
this.state = {
products: [
{ id: 1, name: "Kayak", category: "Watersports", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 },
],
suppliers: [
{ id: 1, name: "Surf Dudes", city: "San Jose", products: [1, 2] },
{ id: 2, name: "Field Supplies", city: "New York", products: [3] },
]
}
There are issues with both of your implementations.
Starting with the top one:
// don't do this
this.setState((collection) => {
return { ...collection, item }
})
In this case, collection is your component state and you're adding a property called item to it. You're going to get this as a result:
{
products: [],
suppliers: [],
item: item
}
The correct way to do this with the spread operator is to return an object that represents the state update. You can use a computed property name to target the appropriate collection:
this.setState((state) => ({
[collection]: [...state[collection], item]
})
)
* Note that both this and the example below are using the implicit return feature of arrow functions. Note the parens around the object.
In the second code sample you're
mutating the existing state directly which you should not do.
returning an array instead of a state update object.
// don't do this
this.setState(state =>
state[collection] = state[collection].concat(item)
);
Assignment expressions return the assigned value, so this code returns an array instead of an object and I'd frankly be surprised if this worked at all.
The correct implementation is the same as above except it uses concat instead of spread to create the new array:
this.setState(state => ({
[collection]: state[collection].concat(item)
})
);
needlessly fancy, arguably silly id generators:
const nextId = (function idGen (start = 100) {
let current = start;
return () => current++;
})(100);
console.log(nextId()); // 100
console.log(nextId()); // 101
console.log(nextId()); // 102
// ----------------
// a literal generator, just for fun
const ids = (function* IdGenerator(start = 300) {
let id = start;
while (true) {
yield id++;
}
})();
console.log(ids.next().value); // 300
console.log(ids.next().value); // 301
console.log(ids.next().value); // 302

Create nested comments array from JSON response

I would like to use the Hacker News Algolia API (exact query) to find comments, and then create a nested JSON structure from the data. I know I need to use recursion, but I am not too sure how.
This is the structure I would like:
comment1
comment2
comment3
comment4
comment5
In JSON:
[
{
"text": "eee",
"children": [
{
"text": "123",
"childeren": [
{
"text": "rew"
}
]
}
]
},
{
"text": "comment4",
"children": []
},
{
"text": "comment5",
"children": []
}
]
The issue is that the API doesn't return comments in the format above. Returned comments have attribute parent_id which is a reference to their parent comment's objectID. So if you have the following nested objectIDs:
foo
bar
foobar
foobar's parent_id is bar and bar's parent_id is foo. And finally foo's parent_id is the Hacker News post ID, in this case 24120336.
What I have so far [repl]:
import axios from 'axios'
interface Comment {
created_at: string;
author: string;
comment_text: string;
story_id: number;
story_title: string;
story_url: string;
parent_id: number | null;
objectID: string;
}
function getChildren(comment: Comment, allComments: Comment[]) {
const children = allComments.filter(
(c) => String(c.parent_id) === comment.objectID
);
const fullChildren = children.forEach((child) =>
getChildren(child, allComments)
);
return fullChildren;
}
const main = async () => {
const { data } =
await axios.get<{ hits: Comment[] }>(
"https://hn.algolia.com/api/v1/search?tags=comment,story_24120336"
);
data.hits.forEach((comment) => {
// Check if comment is top level
if (String(comment.parent_id) === "24120336") {
console.log(getChildren(comment, data.hits));
}
});
}
main()
forEach is for looping through an Array. Use map to convert an Array to a new Array. Try:
function getChildren(comment: Comment, allComments: Comment[]) {
return {
...comment,
children: allComments
.filter(c => String(c.parent_id) === comment.objectID)
.map(c => getChildren(c, allComments))
};
}
...which will give you the nested array of arrays you want.
However I'd recommend a different approach. Note that for each child you're looping through the entire collection. It's far more efficient to do one pass through the Array to collect the parent/child relationships:
const byParent = new Map<string, Array<Comment>>();
for (const comment of allComments) {
let children = byParent.get(comment.parent_id);
if (!children) {
children = [];
byParent.set(comment.parent_id, children);
}
children.push(comment);
}
Now you can do a byParent.get(comment.objectID) at any time to get child comments and do so recursively when necessary:
function getChildren(comment: Comment) {
return {
...comment,
children: byParent.get(comment.objectID)?.map(getChildren)
};
}

Create new object from array

I'm trying to create new object with different properties name from Array.
Array is:
profiles: Array(1)
0:
column:
name: "profileName"
title: "Profile name"
status: "Active"
I want to create new function that return object with two properties:
id: 'profileName',
profileStatus: 'Active'
The function that I have create is returning only one property as undefined undefined=undefined.
function getProfile(profiles) {
if (!profiles.length) return undefined;
return profiles.reduce((obj, profile) => {
console.log('profiles', profile);
return ({
...obj,
id: profile.column.name,
profileStatus: profile.status,
});
}, {});
}
The function getProfile is taking as input array 'profiles' from outside,
I've just tested here and this seems to be working actually
const getProfile1 = (p) => p.reduce((obj, profile) =>({
...obj,
id: profile.column.name,
profileStatus: profile.status,
}), {});
You can use map as an alternative.
var profiles = [{"column":{"name": "profileName3","title": "3Profile name"},"status": "Active"},{"column":{"name": "profileName","title": "Profile name"},"status": "Active"}];
function getProfile(profiles) {
if (!profiles.length) return undefined;
return profiles.map(function(profile,v){
return {id:profile.column.name,profileStatus: profile.status};
});
}
console.log(getProfile(profiles));
Whenever I use reduce in this way, I usually index the final object by some sort of an id. As noted in another answer, you could use map in this situation as well. If you really want your final data structure to be an object, however, you could do something like this:
/**
* returns object indexed by profile id
*/
const formatProfiles = (profiles) => {
return profiles.reduce((obj, profile) => {
return {
...obj,
[profile.id]: {
id: profile.column.name,
profileStatus: profile.status,
}
};
}, {});
};
const profiles = [
{
id: 0,
status: 'active',
column: {
name: "profile_name_1",
title: "profile_title_1",
},
},
{
id: 1,
status: 'inactive',
column: {
name: "profile_name_2",
title: "profile_title_2",
}
}
];
const result = formatProfiles(profiles);
/**
* Result would look like this:
*/
// {
// '0': { id: 'profile_name_1', profileStatus: 'active' },
// '1': { id: 'profile_name_2', profileStatus: 'inactive' }
// }

Rxjs filter observable with array of filters

I'm here because i have a problem for filtering with Rxjs.
I'm trying to filter an observable of products with an array of filters...
Let me explain, I would like to set the result of my filtering to filteredProducts.
For filtering i have to check, for each filter, if the product's filter array contains the name of the filter and if the products values array's contains filter id.
For the moment, the filter works but only with the last selected filter and i'd like to filter products list with all filters in my selectedFilters array
export class ProductsFilterComponent extends BaseComponent implements OnInit {
#Select(FiltersState.getAllFilters) filters$: Observable<any>;
#Input() products$: Observable<Product[]>;
filteredProducts$: Observable<Product[]>;
public selectedFilters = [];
constructor(
private store: Store) { super(); }
ngOnInit() {
this.store.dispatch(new GetAllFilters());
}
private filterProducts() {
this.filteredProducts$ = this.products$.pipe(
map(
productsArr => productsArr.filter(
p =>
p.filters.some(f => this.selectedFilters.some(([selectedF]) => selectedF === f.name.toLowerCase()) // Filter name
&& f.values.some(value => this.selectedFilters.some(([, filterId]) => filterId === value)) // Filter id
)
)
)
);
this.filteredProducts$.subscribe(res => console.log('filtered:', res));
}
}
Here's the structure of a product object
Here's the structure of selectedFilters
A big thank you in advance :-).
I think you have to change selectedFilters to BehaviorSubject and use that with products$ observable. There is combineLatest function which listens for latest values of multiple observables and returns an array
Example
window.products$ = rxjs.of([
{
id: 1,
name: "product 1",
category: {
id: 1,
name: 'test'
},
filters: [
{
name: "test1",
values: [1]
}
],
url: '/assets/test1.png'
},
{
id: 2,
name: "product 2",
category: {
id: 1,
name: 'test'
},
filters: [
{
name: "test2",
values: [2]
}
],
url: '/assets/test2.png'
}
])
window.filteredProducts$ = null;
window.selectedFilters = new rxjs.BehaviorSubject([])
function filterProducts() {
filteredProducts$ = rxjs.combineLatest(products$, selectedFilters)
.pipe(
rxjs.operators.map(
([products, filters]) => products.filter(
product =>
product.filters.some(filter => filters.some(([filterName]) => filterName === filter.name.toLowerCase())
&& filter.values.some(value => filters.some(([, filterId]) => filterId === value))
)
)
)
);
filteredProducts$.subscribe(res => console.log('filtered:', res));
}
filterProducts()
window.selectedFilters.next([...window.selectedFilters.value, ['test1', 1]]);
window.selectedFilters.next([...window.selectedFilters.value, ['test2', 2]]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>
Your code seems fine, but can be modified a bit.
For start, I would clean the predicate, you can use lodash intersectionWith function to intersect two arrays with different values.
Also, you can use mergeAll operator to iterate filter by filter, which makes this look less nested.
Overall it should look something like this:
products$
.pipe(
mergeAll(),
filter(
product =>
_.intersectionWith(
product.filters,
selectedFilters,
(filter, [name, id]) =>
filter.name.toLowerCase() === name && filter.values.includes(id)
).length > 0
)
)
.subscribe(console.log);
You can run the full example code here
There's is the error i get. And the lines where there's the error.

React setState of for deeply nested value

I’ve got a very deeply nested object in my React state. The aim is to change a value from a child node. The path to what node should be updated is already solved, and I use helper variables to access this path within my setState.
Anyway, I really struggle to do setState within this nested beast. I abstracted this problem in a codepen:
https://codesandbox.io/s/dazzling-villani-ddci9
In this example I want to change the child’s changed property of the child having the id def1234.
As mentioned the path is given: Fixed Path values: Members, skills and variable Path values: Unique Key 1 (coming from const currentGroupKey and both Array position in the data coming from const path
This is my state object:
constructor(props) {
super(props);
this.state = {
group:
{
"Unique Key 1": {
"Members": [
{
"name": "Jack",
"id": "1234",
"skills": [
{
"name": "programming",
"id": "13371234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "abc1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
},
{
"name": "Black",
"id": "5678",
"skills": [
{
"name": "programming",
"id": "14771234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "def1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
}
]
}
}
};
}
handleClick = () => {
const currentGroupKey = 'Unique Key 1';
const path = [1, 1];
// full path: [currentGroupKey, 'Members', path[0], 'skills', path[1]]
// result in: { name: "writing", id: "def1234", changed: "2019-08-28T19:25:46+02:00" }
// so far my approach (not working) also its objects only should be [] for arrays
this.setState(prevState => ({
group: {
...prevState.group,
[currentGroupKey]: {
...prevState.group[currentGroupKey],
Members: {
...prevState.group[currentGroupKey].Members,
[path[0]]: {
...prevState.group[currentGroupKey].Members[path[0]],
skills: {
...prevState.group[currentGroupKey].Members[path[0]].skills,
[path[1]]: {
...prevState.group[currentGroupKey].Members[path[0]].skills[
path[1]
],
changed: 'just now',
},
},
},
},
},
},
}));
};
render() {
return (
<div>
<p>{this.state.group}</p>
<button onClick={this.handleClick}>Change Time</button>
</div>
);
}
I would appreciate any help. I’m in struggle for 2 days already :/
Before using new dependencies and having to learn them you could write a helper function to deal with updating deeply nested values.
I use the following helper:
//helper to safely get properties
// get({hi},['hi','doesNotExist'],defaultValue)
const get = (object, path, defaultValue) => {
const recur = (object, path) => {
if (object === undefined) {
return defaultValue;
}
if (path.length === 0) {
return object;
}
return recur(object[path[0]], path.slice(1));
};
return recur(object, path);
};
//returns new state passing get(state,statePath) to modifier
const reduceStatePath = (
state,
statePath,
modifier
) => {
const recur = (result, path) => {
const key = path[0];
if (path.length === 0) {
return modifier(get(state, statePath));
}
return Array.isArray(result)
? result.map((item, index) =>
index === Number(key)
? recur(item, path.slice(1))
: item
)
: {
...result,
[key]: recur(result[key], path.slice(1)),
};
};
const newState = recur(state, statePath);
return get(state, statePath) === get(newState, statePath)
? state
: newState;
};
//use example
const state = {
one: [
{ two: 22 },
{
three: {
four: 22,
},
},
],
};
const newState = reduceStatePath(
state,
//pass state.one[1],three.four to modifier function
['one', 1, 'three', 'four'],
//gets state.one[1].three.four and sets it in the
//new state with the return value
i => i + 1 // add one to state.one[0].three.four
);
console.log('new state', newState.one[1].three.four);
console.log('old state', state.one[1].three.four);
console.log(
'other keys are same:',
state.one[0] === newState.one[0]
);
If you need to update a deeply nested property inside of your state, you could use something like the set function from lodash, for example:
import set from 'lodash/set'
// ...
handleClick = () => {
const currentGroupKey = 'Unique Key';
const path = [1, 1];
let nextState = {...this.state}
// as rightly pointed by #HMR in the comments,
// use an array instead of string interpolation
// for a safer approach
set(
nextState,
["group", currentGroupKey, "Members", path[0], "skills", path[1], "changed"],
"just now"
);
this.setState(nextState)
}
This does the trick, but since set mutates the original object, make sure to make a copy with the object spread technique.
Also, in your CodeSandbox example, you set the group property inside of your state to a string. Make sure you take that JSON string and construct a proper JavaScript object with it so that you can use it in your state.
constructor(props) {
super(props)
this.setState = { group: JSON.parse(myState) }
}
Here's a working example:
CodeSandbox

Categories

Resources