Context
Hi,
I'm creating a form to create virtual machine. Informations are gathered across multiple pages (components) and centralized in top level component.
It's state object looks like this :
const [vmProp, setVmProp] = useState({
name: "",
image_id: "",
image_name: "",
volumesList: [],
RAM: "",
vcpu: "",
autostart: true,
});
Problem
I want to be able to add/remove a volume to the volumes List and I want volumesList to looks like this :
[
{
name: "main",
size: 1024
},
{
name: "backup",
size: 2048
},
{
name: "export",
size: 2048
}
]
What I've tried
For now I've only tried to add a volume.
1 : working but does not produce what I want
const handleAddVolume = (obj) => {
setVmProp({ ...vmProp,
volumesList: {
...vmProp.volumesList,
[obj.name]: {
size: obj.size,
},
} });
};
It's working but the output is :
[
name: {
size: 1024
}
]
2 : should be working but it's not
const handleAddVolume = (obj) => {
setVmProp({ ...vmProp,
volumesList: {
...vmProp.volumesList,
{
name: obj.name,
size: obj.size,
},
} });
};
output :
[
name: "main",
size: 1024
]
Do you have any ideas on how to handle such state object ?
Thanks
You'll have a better time if you don't need to nest/unnest the volumes list from the other state. (Even more idiomatic would be to have a state atom for each of these values, but for simple values such as booleans and strings, this will do.)
const [vmProps, setVmProps] = useState({
name: "",
image_id: "",
image_name: "",
RAM: "",
vcpu: "",
autostart: true,
});
const [volumesList, setVolumesList] = useState([]);
volumesList shouldn't be object, make it as an array inside handleAddVolume function.
Try the below approach,
const handleAddVolume = (obj) => {
setVmProp({ ...vmProp,
volumesList: [
...vmProp.volumesList,
{
name: obj.name,
size: obj.size,
}
] });
};
Here is a working example:
const Example = (props) => {
const inputRef = createRef();
const [vmProp, setVmProp] = useState({
name: "",
image_id: "",
image_name: "",
volumesList: [],
RAM: "",
vcpu: "",
autostart: true,
});
const { volumesList } = vmProp;
const handleAddVolume = (e) => {
e.preventDefault();
const input = inputRef.current.value;
setVmProp((prevVmProp) => {
const newVmProp = { ...prevVmProp };
newVmProp.volumesList.push({
name: input,
size: 1024,
});
return newVmProp;
});
// reset the input
inputRef.current.value = "";
};
return (
<>
<form>
<input ref={inputRef} type="text" />
<button onClick={handleAddVolume}>Add volume</button>
</form>
{volumesList.map((volume) => (
<div key={volume.name}>{`${volume.name} - ${volume.size}`}</div>
))}
</>
);
};
export default Example;
it's just a proof of concept. Basically, setVmProp accepts a function that gets the up-to-date state value as the update is asynchornus and returns the new value of the state. so using ES6's destructuring function, I make a copy of the latest value of vmProp called newVmProp , then push a new object to newVmProp.volumesList then return newVmProp that contains the newly added volume + everything else. The value of the name I get from an input field. Again this is just a proof of concept.
As I needed to be able to add / remove / replace an object in my array I decided to create it's own state as following :
const [volumesList, setVolumesList] = useState([]);
const handleAddVolume = (obj) => {
setVolumesList((oldList) => [...oldList, obj]);
};
const handleRemoveVolume = (obj) => {
setVolumesList((oldList) => oldList.filter((item) => item.name !== obj.name));
};
const handleEditVolume = (obj) => {
setVolumesList(
volumesList.map((volume) =>
(volume.name === obj.name
? { ...volume, ...obj }
: volume),
),
);
};
Thanks for all your answers !
Related
I have this state
this.state = {
dropdown1: false,
dropdown2: false,
dropdown3: false
}
I want to access to these dropdowns in state using this.setState but the number after 'dropdown' comes from API
onMaca = (ev) => {
this.setState({
dropdown + ev: true
})
}
So I want the key to be dynamic 'dropdown1' for example.
Thanks for your answers
you can access the object property like this object['property name']
onMaca = (ev) => {
this.state['dropdown' + ev]= true;
this.setState({
...this.state
})
}
https://codezup.com/add-dynamic-key-to-object-property-in-javascript/
You can use any of these to set key dynamically. I will try to update the answer with an example in a while for setState.
The state is a JS object, so you can get its keys as usual, like so:
const stateKeys = this.state.keys()
Now you have an array: [ "dropdown1", "dropdown1", "dropdown1" ]
One way to use it would be:
const keysMap = statekeys.map(( item, i ) => return {
key: item,
idx: i,
number: item.replace( /dropdown/, '' )
}
keysMap will look like so: [ { key: 'dropdown1', idx: 0, number "1" }, { key: 'dropdown1', idx: 1, number "2" }, { key: 'dropdown1', idx: 2, number "3" } ]
You can query keysMap for a given dropDownNumber like so:
let k = keysMap.find( kmap => kmap.key = dropDownNumber )
To set the dropdown's state:
this.setState({ k: <whatever> })
I have a question I am making React app. The thing is that in useEffect I loop through six items every time when only one thing changes. How to solve it to change only one variable which was changed in reducer function not looping for 6 items when only one was changed, or is it okay to keep code like this?
const initialReducerValue = {
name: {
val: '',
isValid: false,
},
lastName: {
vaL: '',
isValid: false
},
phoneNumber: {
val: '',
isValid: false
},
city: {
val: '',
isValid: false,
},
street: {
val: '',
isValid: false
},
postal: {
val: '',
isValid: false
},
}
const OrderForm = () => {
const orderReducer = (state, action) => {
if (action.type === 'HANDLE TEXT CHANGE') {
return {
...state,
[action.field]: {
val: action.payload,
isValid: true
}
}
}
}
const [formState, formDispatch] = useReducer(orderReducer, initialReducerValue)
const [formIsValid, setFormIsValid] = useState(false)
const changeTextHandler = (e) => {
formDispatch({
type: 'HANDLE TEXT CHANGE',
field: e.target.name,
payload: e.target.value
})
}
useEffect(() => {
const validationArray = []
for (const key of Object.keys(formState)) {
validationArray.push(formState[key].isValid)
}
const isTrue = validationArray.every(item => item)
setFormIsValid(isTrue)
}, [formState])
This code
const validationArray = []
for (const key of Object.keys(formState)) {
validationArray.push(formState[key].isValid)
}
const isTrue = validationArray.every(item => item)
is equivalent to
const isTrue = Object.values(formState).every(item => item.isValid);
This still iterates over all items when only one was changed, but with a temporary array less.
For six items, I would not spend time trying to optimize this code further, but that's your choice.
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
Here is my state
const initState = [
{
id: 1,
criteria: [],
isInclusion: true,
},
{
id: 2,
criteria: [],
isInclusion: true,
},
];
I am trying to add a new object into criteria array with my dispatch
dispatch(
addCriteria({
id: item.id,
type: item.type,
groupId: 1,
})
);
in the reducer I do the following but it doesnt add to an array of an existing group but instead, it adds as a new object to the array with that criteria added in it.
const addReducer = (state = initState, action) => {
switch (action.type) {
case QUERYGROUPS.ADD_CRITERIA: {
const newCriteria = {
id: action.payload.id,
type: action.payload.type,
};
const newState = [
...state,
state.map(group =>
group.id === action.payload.groupId
? {
...group,
criteria: newCriteria,
}
: group
),
];
return newState;
}
}
};
You are adding all items to the state object (also the one you want to modify) with ...state and than you map over it again. This will not work.
You should change your state object to be an object to access the items by reference and not index and use the id as access. This will also be faster(thanks #Yash Joshi ):
const initState = {
1: {
criteria: [],
isInclusion: true,
},
2: {
criteria: [],
isInclusion: true,
},
};
This will let you access and update the state more easily and easier to stay immutable.
This will let you update it like this:
case QUERYGROUPS.ADD_CRITERIA: {
const newCriteria = {
id: action.payload.id,
type: action.payload.type,
};
const newState = {
...state,
[action.payload.groupId]: {
...state[action.payload.groupId],
criteria: [
...state[action.payload.groupId].criteria,
newCriteria
],
}
};
return newState;
To add a new item to it:
const newState = {
...state,
[action.payload.groupId]: {
isInclusion: false,
criteria: [ ],
}
};
Hope this helps. Happy coding.
Try spread operator (es6):
return [
...state,
newCriteria,
]
This will return a new array with the newCriteria object on it.
Here is how I'd do it:
const newCriteria = {
id: action.payload.id,
type: action.payload.type,
};
const newState = state.map(gr => {
if (gr.id !== action.payload.groupId) {
return gr;
}
return {
...gr,
criteria: [...gr.criteria, newCriteria];
}
});
I think you should follow Domino987 approach it will be faster. But if you still wish to continue with your approach. You can try this:
const newState = state.map(item => item.id === newItem.id ? ({ ...item, ...newItem }) : item );
return newState;
Hope this Helps!
I am trying to rename 'timestamp' in my array with key/value pairs. Currently, I am adding id and assinging a value but I also need to change 'timestamp' to 'start'. Is it possible to do all that at once?
Here is what I have so far:
const { data } = this.state
const newAction = data.action.map((actionItem, index) => ({
...actionItem,
id: index + 1,
...actionItem,
'start': actionItem.timestamp
}));
const items = {
...data,
action: newAction
};
Data Structure:
Update:
So, instead of replacing timestamp with start, the code above adds start to array. I want timestamp to be named start.
Another approach with spread syntax.
const action = [
{
Second: [],
action: "Program Executed",
timestamp: 12345,
},
{
Second: [],
action: "Something Happened",
timestamp: 67891,
}
];
const newAction = action.map( ( actionItem, index ) => {
const { timestamp: start, ...rest } = actionItem;
return {
...rest,
id: index + 1,
start,
}
});
console.log( newAction );