Update ngrx state in nested object Angular - javascript

I would be able to update an object nested in another object in my application but i got some problems.
Let's assume that the entity that I want to update is something like this:
{
{
"id": 0,
"name": "Yellow Car",
"details": {
"engine": {},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
I can easily update some part of this entity in this way:
let car: Car = {
...car,
...this.form.value
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car
};
this.store.dispatch(carUpdated({carUpdate}));
But in this way I can only update name, created, lastUpdate and I can't update the nested object details. What happens if I try to edit the detail object now? Nothing i wanna happens.
This is the selector:
export const carUpdated = createAction(
"[Edit Car] Car Updated",
props<{carUpdate: Update<Car>}>()
);
The effect:
saveCar$ = createEffect(
() => this.actions$
.pipe(
ofType(CarActions.carUpdated),
concatMap(action => this.carService.editCar(
action.carUpdate.changes,
action.carUpdate.id
))
),
{dispatch: false}
)
The reducer:
on(CarActions.carUpdated, (state, action) =>
adapter.updateOne(action.carUpdate, state)),
The service sends to the backend the right data and it's working good without the state management.
What I am doing now is retrieve the single carObject in this way in the component in the ngOnInit
car$ = this.store.pipe(
select(selectCar(id))
)
and the selector is:
export const selectCar = (id) => createSelector(
selectAllCars,
(cars: any) => cars.filter((car) => {
let filteredCar = car.id == id;
if(filteredCar) {
return car;
}
}).map(car => car)
);
and then after my edit I can use the dispatch to confirm my edit
this.store.dispatch(carUpdated({carUpdate}));
but as I said if I try to update the details object i have this
let car: Car = {
...car, // the entire car object
...this.form.value // only the details
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car //the details are mixed with the car object and not inside the object itself
};
something like this:
{
"id": 0,
"name": "Yellow Car",
"engine": {},
"details": {
},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
Is there an easy way to fix this?

I'll try to provide a general answer which might show techniques on how to update one object with another object.
If in the form, you only have the details as update (update is Partial<Car["details"]>, you can do this:
const update: Partial<Car["details"]> = this.form.value;
const newCar: Car = {
...oldCar,
details: {
...oldCar.details,
...update
}
Then there's the possibility that the update is (partial) Car with partial details:
const update: Partial<Car> & { details: Partial<Car["details"]> } = this.form.value;
const newCar: Car = {
...oldCar,
...update,
details: {
...oldCar.details,
...update.details
}
}
An improbable option is that you have mixed detail and car properties in the update (for whatever reason - you might be doing something wrong). Then you can pick them by hand. Note that this will delete old values if new values are undefined.
const update: Pick<Car, 'name' | 'lastUpdate'> & Pick<Car["details"], 'ownerInfo'> = this.form.value;
const newCar: Car = {
...oldCar,
name: update.name,
lastUpdate: update.lastUpdate,
name: update.name
details: {
...oldCar.details,
ownerInfo: details.ownerInfo
}
}

Related

ReactJS and creating ‘object of objects’ using state hooks?

I'm trying to create an 'object of objects' in ReactJS using state hooks, but I'm unsure how to dynamically create it based on the data coming in.
The data arrives on a websocket, which I have placed in a Context and is being used by the component in question. The JSON data hits the onmessage, it invokes my useEffect state hook to then call a function to update the useState variable accordingly.
The inbound websocket data messages come in one at a time and look something like this (important keys listed, but there lots more props inside them) :
{
"name": "PipelineA",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineE",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineZ",
"state": "succeeded",
"group": "Group4"
}
...where the name and group are the values I want to use to create an 'object of objects'. So the group will be used to create a group of pipelines that are all part of that same group, which within that object, each pipeline will have its name as the 'key' for its entire data. So, the end state of the ‘object of objects’ would look something like this:
{
"Group1": {
"PipelineA": {
"name": "PipelineA",
"state": "running",
"group": "Group1"
},
"PipelineB": {
"name": "PipelineB",
"state": "running",
"group": "Group1"
}
},
"Group2": {
"PipelineC": {
"name": "PipelineC",
"state": "running",
"group": "Group2"
},
"PipelineD": {
"name": "PipelineD",
"state": "running",
"group": "Group2"
}
},
...etc...
}
So the idea being, pipelines of Group1 will be added to the Group1 object, if PipelineA already exists, it just overwrites it, if it does not, it adds it. And so on and so on.
I'm (somewhat) fine with doing this outside of React in plain JS, but I cannot for the life of me figure out how to do it in ReactJS.
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const [socketState, ready, message, send] = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineTypeObjects(message)
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name
const pipelineGroup = data.group
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects(prevState => ({
...prevState,
[pipelineGroup]: {[pipelineName]: data} // <-- doesnt do what I need
}))
}
And help or suggestions would be appreciated. FYI the pipeline names are unique so no duplicates, hence using them as keys.
Also, why am I doing it this way? I already have it working with just an object of all the pipelines where the pipeline name is the key and its data is the value, which then renders a huge page or expandable table rows. But I need to condense it and have the Groups as the main rows for which I then expand them to reveal the pipelines within. I thought doing this would make it easier to render the components.
It's just that you haven't gone quite far enough. What you have will replace the group entirely, rather than just adding or replacing the relevant pipeline within it. Instead, copy and update the existing group if there is one:
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
Also, you're trying to use iterable destructuring ([]) here:
const [ socketState, ready, message, send ] = useContext(WebsocketContext);
but as I understand it, your context object is a plain object, not an iterable, so you'd want object destructuring ({}):
const { socketState, ready, message, send } = useContext(WebsocketContext);
Live Example:
const { useState, useEffect, useContext } = React;
const WebsocketContext = React.createContext({ message: null });
const Example = () => {
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const { socketState, ready, message, send } = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineGroupObjects(message);
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
return <pre>{JSON.stringify(groupedPipelineObjects, null, 4)}</pre>;
};
// Mocked messages from web socket
const messages = [
{
name: "PipelineA",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineB",
state: "running",
group: "Group1",
},
{
name: "PipelineC",
state: "running",
group: "Group2",
},
{
name: "PipelineD",
state: "running",
group: "Group2",
},
{
name: "PipelineE",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineZ",
state: "succeeded",
group: "Group4",
},
];
const App = () => {
const [fakeSocketContext, setFakeSocketContext] = useState({ message: null });
useEffect(() => {
let timer = 0;
let index = 0;
tick();
function tick() {
const message = messages[index];
if (message) {
setFakeSocketContext({ message });
++index;
timer = setTimeout(tick, 800);
}
}
return () => {
clearTimeout(timer);
};
}, []);
return (
<WebsocketContext.Provider value={fakeSocketContext}>
<Example />
</WebsocketContext.Provider>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

React component isn't rendering after updating state by useState

I want to update an state object userInfo which contains an array named work which is an array of objects
My Database object userInfo looks like this
{
"_id":"61a6a1d64707c03465eae052",
"fullName": "Md Nurul Islam",
"works":
[
{
"isEditing": false,
"_id": "61a6a1d64707c03465eae053",
"company": "Amazon Logistics",
"position": "Sortation Associate",
"isCurrent": true
},
{
"isEditing": false,
"_id":"61a6a1d64707c03465eae054",
"company": "The Rani Indian Takeway",
"position": "Customer Service Assistant",
"isCurrent": false,
}
]
}
But I don't want update the database, i just want to update the state userInfo in frontend to go editing mood, for this when the user clicks edit next to work section, the properties isEditing will be toggled.
Below is my code
const [userInfo, setUserInfo] = useState(user);
const workToggleHandler = (work) =>
{
setUserInfo((prev) => {
prev.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
});
return prev;
});
With this code, my state object is updated But my components are not re-rendered
you can put into the useEffect when particular state is updated then that effect will call
useEffect(()=>{
your code will go here
}[userInfo]),
I just changed my code on workToggleHandler function and its working completely as i want.
Below my code
const [userInfo, setUserInfo] = useState(user);
const workToggleHandler = (work) => {
setUserInfo({
...userInfo,
works: userInfo.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
}),
});
};
React does not look at the data of the object but rather on the pointer of the object. Since this is not changed (only "prev" is edited, no new object with a new object address), it doesn't not rerender.
A solution would be to copy the object.
let newUserInfo = Object.assign({}, prev);
newUserInfo.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
});
return newUserInfo;
Disclaimer: Maybe a deep copy is necessary....

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

#ngrx/data - Undoing an optimistic delete, should an UNDO_ONE action revert the changeState?

Stackblitz
In #ngrx/data doing an optimistic delete (of our Hero "Ant-Man") causes changeState to be updated as shown below:
{
"entityCache": {
"Hero": {
"ids": [1, 2, 3, 5, 6],
"entities": {
"1": {
"id": 1,
"name": "Spiderman",
"power": 1
},
"2": {
"id": 2,
"name": "Thor",
"power": 5
},
"3": {
"id": 3,
"name": "Hulk",
"power": 6
},
"5": {
"id": 5,
"name": "Iron Man",
"power": 9
},
"6": {
"id": 6,
"name": "Thanos",
"power": 10
}
},
"entityName": "Hero",
"filter": "",
"loaded": true,
"loading": true,
"changeState": {
"4": {
"changeType": 2,
"originalValue": {
"id": 4,
"name": "Ant-Man",
"power": 7
}
}
}
}
}
}
Using the effect below I've fired an UNDO_ONE when the delete fails due to a http request error:
deleteError$ = createEffect(() => {
return this.actions$.pipe(
ofEntityType("Hero"),
ofEntityOp([EntityOp.SAVE_DELETE_ONE_ERROR]),
map(action => {
const id = action.payload.data.originalAction.payload.data;
const options: EntityActionOptions = {
// tried various values
}
return new EntityActionFactory().create( <-----------------------dispatch UNDO_ONE action-----------
"Hero",
EntityOp.UNDO_ONE,
id,
options
);
})
);
});
Question: Should dispatching an UNDO_ONE action revert the changeState
i.e. remove the changes to this part of the entities state caused by a delete action?
If so, how do you correctly dispatch an UNDO_ONE and what arguments are required?
I've explored different values for both data and options for the EntityActionFactory.create() method:
EntityActionFactory.create<P = any>(entityName: string, entityOp: EntityOp, data?: P, options?: EntityActionOptions): EntityAction<P>
Here I'm doing an optimistic delete and on a SAVE_DELETE_ONE_ERROR dispatching an UNDO_ONE action via an effect.
When I swap out UNDO_ONE for UNDO_ALL changeState does revert back to {} which gives me cause to think changeState should revert back to {} given we're cancelling the delete.
According to the documentation here, it should :
The undo operations replace entities in the collection based on information in the changeState map, reverting them their last known server-side state, and removing them from the changeState map. These entities become "unchanged."
In order to overcome this issue, you can create a metaReducer which removes the relevant modifications remaining in the changeState after an undo action. Here is the content of my entity-metadata.ts with the relevant metareducer.
import { EntityMetadataMap, EntityDataModuleConfig, EntityCache } from '#ngrx/data';
import { MetaReducer, ActionReducer, Action } from '#ngrx/store';
const entityMetadata: EntityMetadataMap = {};
const pluralNames = {};
const objectWithoutProperties = (obj, keys) => {
const target = {};
for (const i in obj) {
if (keys.indexOf(i) >= 0) { continue; }
if (!Object.prototype.hasOwnProperty.call(obj, i)) { continue; }
target[i] = obj[i];
}
return target;
};
function revertStateChanges(reducer: ActionReducer<any>): ActionReducer<any> {
return (state, action: any) => {
if (action.type.includes('#ngrx/data/undo-one')) {
// Note that you need to execute the reducer first if you have an effect to add back a failed removal
state = reducer(state, action);
const updatedChangeState = objectWithoutProperties(state[action.payload.entityName].changeState, [action.payload.data.toString()]);
const updatedState = {
...state,
[action.payload.entityName]: {
...state[action.payload.entityName],
changeState: updatedChangeState
}
};
return reducer(updatedState, action);
}
return reducer(state, action);
};
}
const entityCacheMetaReducers: MetaReducer<EntityCache, Action>[] = [revertStateChanges];
export const entityConfig: EntityDataModuleConfig = {
entityMetadata,
pluralNames,
entityCacheMetaReducers
};
There might be a better way of writing this code (in particular the way I handled the override changeState property) but for my case it proved to work.
Moreover, it might need some updates in order to handle the different undo cases, as, when I wrote it I just needed to make it work for an undo concerning a delete action, where action.payload.data is the entity id.

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