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
Related
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>
hoping this is a easy question but I can't seem to figure it out.
I'm attempting to append a key to a object that is held in state. This key and value pair don't exist prior which I think is whats giving me the problem. Anyways here's what I have so far:
const Reducer = (state, action) => {
switch (action.type) {
case "SET_STUDENT_TAGS":
const userID = action.payload.id;
let studentIndex = state.originalStudentList.students.findIndex(obj => obj.id === action.payload.id);
return {
...state,
originalStudentList: {
...state.originalStudentList,
students: {
[studentIndex]: {
tags: "test"
}
}
}
};
Now the issue i'm running into is that I can run findIndex once but any attempt after just crashes my app saying its not a function. Simiarly the set state logic in the reducer there doesn't work either as it can't find "tags". So I guess i'm at a loss of how to do this. Here's how the data that is getting fed in looks:
{
"students": [
{
"id": "1",
},
{
"id": "2",
}
]
}
here's the function from the input box:
const addTag = (event, student) => {
if (event.key === "Enter") {
if (event.target.value != "") {
dispatch({
type: "SET_STUDENT_TAGS",
payload: {
id: student.id,
value: event.target.value,
},
});
event.target.value = "";
} else {
return;
}
}
};
what I'd like to achieve:
{
"students": [
{
"id": "1",
"tags": ["cat", "dog"]
},
{
"id": "2",
"tags": ["cat", "parrot"]
}
]
}
So i'm at a lost as to how to do this correctly. Appreciate any help!
You're on the right track, but that syntax replaces the array with a regular object, which is why findIndex fails after the first time. For more info, see this answer.
And then to add a property to the specific student, you just replace it with an object spreading the original and adding the tags:
const Reducer = (state, action) => {
switch (action.type) {
case "SET_STUDENT_TAGS":
const userID = action.payload.id;
let studentIndex = state.originalStudentList.students.findIndex(obj => obj.id === action.payload.id);
let student = state.originalStudentList.students[studentIndex];
return {
...state,
originalStudentList: {
...state.originalStudentList,
students: [
state.originalStudentList.students.slice(0, studentIndex),
{
...student,
tags: action.payload.tags
},
state.originalStudentList.students.slice(studentIndex+1)
]
}
};
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)
};
}
I am trying to move an object from one array to another. Think of it like adding / moving a friend from non-friend to friend. I have two arrays, which can be seen below, and I am trying to move an object (i.e. a friend) from possible to current via it's 'id'. In the below example, I am trying to move Parker from possible to current with id = 2.
state = {
current: [
{
id: 1,
name: 'peter'
}
],
possible: [
{
id: 2,
name: 'parker'
}
]
}
function addFriend(state, action) {
const { current, possible } = state;
const addedFriend = Object.assign(
{},
state.possible.splice(action.payload.index, 1)
);
current.push(addedFriend);
const newState = { current, possible };
return newState;
}
Since you can remove multiple elements with splice(), it returns an array. Index the result to get the specific object. You don't need to use Object.assign(), that just copies the value (which just converts the array into an object whose properties are the array indexes).
var state = {
current: [
{
id: 1,
name: 'peter'
}
],
possible: [
{
id: 2,
name: 'parker'
}
]
};
function addFriend(state, action) {
const { current, possible } = state;
const addedFriend = state.possible.splice(action.payload.index, 1)[0];
current.push(addedFriend);
const newState = { current, possible };
return newState;
}
state = addFriend(state, {payload: { index: 0 }});
console.log(state);
I'm not sure why you're returning a new state object, since you're modifying the old state in place.
It is not that time-efficient if you want a fast running code. But it follows immutability.
We just ignore the item from possible, and add it to current.
state = {
current: [
{
id: 1,
name: 'peter'
}
],
possible: [
{
id: 2,
name: 'parker'
}
]
}
function addFriend(state, action) {
const { current, possible } = state;
return {
...state,
current: current.concat(possible[action.payload.index]),
possible: possible.filter((_, index) => index !== action.payload.index)
}
}
state = addFriend(state, {payload: {index: 0}})
console.log(state)
I am deleting an one id in an array, how do I setState after filtering it here?
https://codesandbox.io/s/react-example-1m2qn
const Debtors = () => {
const debtors = [
{
id: 1,
name: "John",
relation: "friend",
statement: [
{ id: 1, date: 2010, amount: "1000", purpose: "John" },
{ id: 2, date: 2014, amount: "2000", purpose: "john" }
]
},
,
{
id: 2,
name: "Jack",
relation: "Friend",
statement: [
{ id: 1, date: 2010, amount: "1000", purpose: "jack" },
{ id: 2, date: 2014, amount: "2000", purpose: "jack" }
]
}
];
const [newDebtors, setdebtors] = React.useState(debtors);
const handleDelete = (stat, i) => {
const newList = newDebtors[0].statement.filter(x => x.id !== stat.id);
// How to set debtors here ?
// setdebtors({ ...newDebtors, statement[0]: newList });
console.log(newList)
// How to set debtors here ?
There's two problems:
1) You are iterating off the original debtors object in your render, instead of the newDebtors state you created via useState(), which is why there does not appear to be any UI change.
You need: newDebtors[0].statement.map
2) You need to pass in the item index in your handleDelete() so it knows what item in the array to update. You can have the function do something like this:
In the onClick:
<a
href="javascript:;"
onClick={() => handleDelete(stat, i, 0)}
>
In the handleDelete():
const handleDelete = (stat, i, arrayIndex) => {
const updatedDebtors = newDebtors.map((item, index) => {
if (index === arrayIndex) {
return {
...item,
statement: item.statement.filter(
statement => statement.id !== stat.id
)
};
} else {
return item;
}
});
setDebtors(updatedDebtors);
};
See sandbox for full solution: https://codesandbox.io/s/react-example-x7uoh
You should do it like that:
setdebtors((prevState) => {
let newArray = Array.from(prevState); // Copy the array
// Manipulate the array as you wish
return newArray; // return it
});
The problem is you are mutating the array of "debtors" you need to map through the array of debtors and change any properties in the object.
const handleDelete = (stat, i) => {
const newList = newDebtors.map((debtor, i) => {
if (i === 0) {
debtor.statement = debtor.statement.filter(x => x.id !== stat.id);
}
return debtor;
});
setdebtors(newList);};
An even better approach is to use "useReducer" which is used for mutating more complex pieces of state, like you have here. THe docs are very helpful useReducer
Hmm I dont know what exactly you are trying to do,
Is this what you are looking for?
const handleDelete = (stat, i) => {
const newList = newDebtors[0].statement.filter(x => x.id !== stat.id);
const newFirstItem = {...newDebtors[0],statement: newList}
const newDebtorList = newDebtors.filter(x => x.id !== newFirstItem.id);
newDebtorList.unshift(newFirstItem);
setdebtors(newDebtorList);
}
I know this seems complex but you kinda actually need to do this as you cannot mutate an array in the state...
What I did here is I first created a new statement list(newList), then created a newFirstItem to be set as the new newDebtors[0], then created a new array(newDebtorList) of all the elements of newDebtors except the first one, I modified this array by pushing the newFirstItem to the 0th position(using unshift)
Finally updated the state with this new array...
hope it helps :)
Note: this is for changing the 0th element if you have the id please change the code accordingly