I have from with input type number, which has an pretty simple onChange function when entered a value the last number is missed out
Example:
If 99 is type in the input box, only 9 is recorded, and when 2523 is typed in the input box 252 is been recorded, the last number is been skipped
Here is the sandbox link
inside of onChange function
const handleRequestingCost = (e, index) => {
setCostsFormValues({
...costsFormValues,
[e.target.name]: [e.target.value]
});
for (const key in costsFormValues) {
const mappedData = {
[key]: costsFormValues[key][0]
};
mappedDataArray.push(mappedData);
const list = [...row];
const item = list[index];
if (item) {
item.requestingCost = mappedDataArray;
setRow(list);
}
}
...
What I am missing here? Or if is this the bad way to do ?
setState is async. So you are reading costsFormValues before it is updated
import "./styles.css";
import { Button, Form } from "react-bootstrap";
import React, { useState } from "react";
export default function App() {
const [costsFormValues, setCostsFormValues] = useState({});
const [row, setRow] = useState([
{
index: 5399,
name: "user name",
quantity: "1000",
remainingQty: 2000,
requestingCost: [],
budgetedCost: [
{
key: "Labour Cost",
value: 176
},
{
key: "Material Cost",
value: 890
}
],
amountForQuantity: [
{
key: "Labour Cost",
value: 150
},
{
key: "Material Cost",
value: 570
}
]
}
]);
const handleRequestingCost = (e, index) => {
const mappedDataArray = [];
setCostsFormValues({
// set all cost value in each array
...costsFormValues,
[e.target.name]: [e.target.value]
});
const updatedFormValues = { // added this local variable which holds latest value update
// set all cost value in each array
...costsFormValues,
[e.target.name]: [e.target.value]
};
for (const key in updatedFormValues) {
// loop each array and merge into one object
const mappedData = {
[key]: updatedFormValues[key][0]
};
mappedDataArray.push(mappedData);
const list = [...row];
const item = list[index];
if (item) {
item.requestingCost = mappedDataArray;
setRow(list);
}
}
console.log(updatedFormValues);
};
Please try to make changes to a local variable rather than setting the state object directly as it takes about 300-500ms to update after the setState() call; as it creates queues for performance optimization.
Please refer to the code snippet:
const handleRequestingCost = (e, index) => {
const mappedDataArray = [];
let TempValue = {};
TempValue = {
...TempValue,
[e.target.name]: [e.target.value]
};
for (const key in TempValue) {
// loop each array and merge into one object
const mappedData = {
[key]: TempValue[key][0]
};
mappedDataArray.push(mappedData);
const list = [...row];
const item = list[index];
if (item) {
item.requestingCost = mappedDataArray;
setRow(list);
}
}
console.log(TempValue);
};
As setState is async, you should use the value just after mutate it, that why we have useEffect hook to do it instead
const handleRequestingCost = (e, index) => {
const mappedDataArray = [];
setCostsFormValues({
// set all cost value in each array
...costsFormValues,
[e.target.name]: [e.target.value]
});
for (const key in costsFormValues) {
// loop each array and merge into one object
const mappedData = {
[key]: costsFormValues[key][0]
};
mappedDataArray.push(mappedData);
const list = [...row];
const item = list[index];
if (item) {
item.requestingCost = mappedDataArray;
setRow(list);
}
}
};
useEffect(() => {
console.log(costsFormValues);
}, [costsFormValues]);
The console will show the right value, but be careful with my snippet, your handleRequestCost do some stuff with costsFormValues you should pass it to useEffect as well
const handleRequestingCost = (e, index) => {
setCostsFormValues({
// set all cost value in each array
...costsFormValues,
[e.target.name]: [e.target.value]
});
};
useEffect(() => {
const mappedDataArray = [];
console.log(costsFormValues);
for (const key in costsFormValues) {
// loop each array and merge into one object
const mappedData = {
[key]: costsFormValues[key][0]
};
mappedDataArray.push(mappedData);
const list = [...row];
const item = list[index];
if (item) {
item.requestingCost = mappedDataArray;
setRow(list);
}
}
}, [costsFormValues]);
Something like this, but there is the index that I didnt get from you logic yet
Related
I want to ask if then is a way to update the whole object in immerjs
For example
I want to set my redux state to the output of this function
Sample state
{
"1":{
// properties
},
"2":{
// properties
}
}
In the Code below mainly, there are 3 function
ObjectPush: what it does is basically shift all properties down by one(eg 1 becomes 2)from the index(param)
ObjectRemove: Remove particular index and change indexes
deleteStrip:Reducer
const ObjectPush = (obj, index) => {
return produce(obj, (state) => {
const keys = Object.keys(state)
.filter((v) => v !== 'misc')
.map(Number);
const draft = Object.assign({}, state);
keys.forEach((key) => {
if (key >= index) {
state[key + 1] = draft[key];
}
});
delete state[index];
});
};
export const ObjectRemove = (obj, index) => {
const state = {};
const updatedState = { ...ObjectPush(obj, index)
};
const keys = Object.keys(updatedState)
.filter((v) => v !== 'misc')
.map(Number);
const temp = Object.assign({}, obj);
keys.map((key) => {
if (temp[key]) {
if (key > index) {
state[key - 1] = temp[key];
} else {
state[key] = temp[key];
}
}
});
return state;
};
// inside immer produce
deleteStrip: (state, action) => {
const { index } = action.payload;
state = ObjectRemove(state, index);
}
I have a form where I put a float value (1.1, 1.2, 1.9 and so on) and I want to store a bunch of them inside an array on an atom:
import { atom } from 'recoil';
export const valueState = atom({
key: 'values',
default: []
});
Whenever I write a value and it's checked that it's a double, the value gets added to valueState, however, I want to make it so if that the value I write on the form gets deleted, it also deletes the value from the valueState array. I tried by using pop, however, if I do so the program crashes. How can I do it then?
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
// Delete v from the atom array here
}
};
pop did not work for you because it does not return a new array (state immutability)
I think you can do a trick with filter. For example
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
Full code
import { valueState as valueStateAtom } from '../../atoms/Atoms';
import { useSetRecoilState } from 'recoil';
const setValue = useSetRecoilState(valueStateAtom);
// The function that handles the onChange event of the form
const setNewValue = (v) => {
if (v !== '') {
const valueNumber = parseFloat(v);
if (!isNaN(valueNumber)) {
setPageValueChanged(true);
pageValue = valueNumber;
// The value gets added to valueState
setValue((prev) => prev.concat({ valueNumber, cardID }));
} else {
setPageValueChanged(false);
}
} else {
setPageValueChanged(false);
setValue((prev) => prev.filter((value, index) => index !== prev.length - 1));
}
};
One more feedback, your concat is seemingly incorrect. It's expecting to have an array param but you passed an object. The modification can be
setValue((prev) => prev.concat([{ valueNumber, cardID }]));
I'm trying to fill an object with values that I'm getting from an array of objects but it's not working as expected.
This is a simplified code example
https://codesandbox.io/s/crazy-nobel-c7xdb?file=/src/App.js
import "./styles.css";
import React, { useEffect, useState } from "react";
export default function App() {
const [fieldsValues, setFieldsValues] = useState({});
const items = [{ value: "a" }, { value: "b" }, { value: "c" }];
useEffect(() => {
items.map((item, index) => {
return setFieldsValues({
...fieldsValues,
[index]: item.value
});
});
}, []);
return (
<div className="App">
<h2> {` fieldsValues = ${JSON.stringify(fieldsValues)}`} </h2>
</div>
);
}
I want the fieldsValues to return this:
{
0: "a",
1: "b",
2: "c"
}
What I'm getting now:
fieldsValues = {"2":"c"}
You fix it by doing this
useEffect(() => {
items.map((item, index) => {
return setFieldsValues((prev) => ({
...prev,
[index]: item.value,
}));
});
}, []);
Better way of doing this is
useEffect(() => {
const data = items.reduce(
(prev, item, index) => ({ ...prev, [index]: item.value }),
{}
);
setFieldsValues((prev) => ({ ...prev, ...data }));
}, []);
To create the object map the array to [index, value] pairs, and convert to an object with Object.fromEntries():
const items = [{ value: "a" }, { value: "b" }, { value: "c" }];
const result = Object.fromEntries(items.map(({ value }, index) => [index, value]))
console.log(result)
However, the way you are using the array, and then need to set the state doesn't actually makes sense in the react context.
If the array is a prop, you should add it to useEffect as a dependency:
const arrToObj = items => Object.fromEntries(items.map(({ value }, index) => [index, value]))
export default function App({ items }) {
const [fieldsValues, setFieldsValues] = useState({});
useEffect(() => {
setState(() => arrToObj(items))
}, [items]);
...
If it's a static array, set it as the initial value of setState:
const arrToObj = items => Object.fromEntries(items.map(({ value }, index) => [index, value]))
const items = [{ value: "a" }, { value: "b" }, { value: "c" }];
export default function App({ items }) {
const [fieldsValues, setFieldsValues] = useState(() => arrToObj(items));
...
By your way It would be like this
useEffect(() => {
let test={}
items.map((item, index) => {
return setFieldsValues((prev)=>{
return {
...prev,
[index]: item.value
}
});
});
}, []);
I have a MenuOptions component that I pass an options prop to. Options is a large array of objects. Each object has a nested array called 'services' inside services is an object with a key 'serviceType' which is the only value I want. I want to take all those values, push them into a new array and remove any duplicates if there are any, and then map through that new array and display each item in an 'option' html tag.
here is my createArray function:
const createNewArray = () => {
let optionsArr = []
let uniqueArr;
options.map((option) => {
option.services.map((service) => optionsArr.push(service.serviceType))
})
uniqueArr = [...new Set(optionsArr)];
return uniqueArr;
}
uniqArr seems to be holding what I want but now I want to set this to a piece of global state. Trying something like this does not work. array seems to still be set as null
const [array, setArray] = useState(null)
useEffect(() => {
setArray(createNewArray())
}, [])
any solutions? Thanks
1) You should add your array state initial value as an empty array:
const [array, setArray] = useState([]);
Live Demo
2) You can simplify the creating of a new array as:
const createNewArray = () => [
...new Set(options.flatMap((o) => o.services.map((obj) => obj.serviceType)))
];
3) set array state in useEffect as:
useEffect(() => {
setArray(createNewArray());
}, []);
From your description is this your data?
const options = [{
services: [
{
serviceType: 'serviceType',
}
],
},{
services: [
{
serviceType: 'serviceType',
}
],
},{
services: [
{
serviceType: 'serviceType',
}
],
},
{
services: [
{
serviceType: 'serviceType',
}
],
}]
here is the solution
const uniq = (a) => [...new Set(a)];
const createNewArray = (array) => {
const c = [...array]
const newArray = []
for (let i = 0; i < c.length; i++) {
const e = c[i];
for (let ii = 0; ii < e.length; ii++) {
const ee = e[ii].serviceType;
newArray.push(ee);
}
}
const toReturn = uniq(newArray)
return toReturn;
}
If you want unique options, just pass the options in and set them to the state after you massage the data.
const { useEffect, useMemo, useState } = React;
const unique = (arr) => [...new Set(arr)];
const uniqueOptions = (options) =>
unique(options.flatMap(({ services }) =>
services.map(({ serviceType }) => serviceType)));
const data = {
options: [
{ services: [{ serviceType: "Home" } , { serviceType: "About" }] },
{ services: [{ serviceType: "About" } , { serviceType: "Help" }] },
{ services: [{ serviceType: "Help" } , { serviceType: "Home" }] },
],
};
const MenuOptions = (props) => {
const { options } = props;
const [opts, setOpts] = useState([]);
useEffect(() => setOpts(uniqueOptions(options)), [options]);
return useMemo(
() => (
<select>
{opts.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
),
[opts]
);
};
const App = ({ title }) =>
useMemo(
() => (
<div>
<h1>Services</h1>
<form>
<MenuOptions options={data.options} />
</form>
</div>
),
[]
);
ReactDOM.render(<App />, document.getElementById("react-app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react-app"></div>
I have the current state as:
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
I map through the state and display the data in a div and call a onClicked function to toggle the isChecked value on click:
const clickData = index => {
const newDatas = [...data];
newDatas[index].isChecked = !newDatas[index].isChecked;
setData(newDatas);
const newSelected = [...selected];
const temp = datas.filter(isChecked==true) // incomplete code, struggling here.
const temp = datas.isChecked ?
};
I have another empty state called clicked:
const[clicked, setClicked] = setState([]). I want to add all the objected whose isChecked is true from the datas array to this array. How can I do this?
I just add checkBox & onChange event instead of using div & onClick event for your understanding
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [data, setData] = useState([
{ id: 1, name: "One", isChecked: false },
{ id: 2, name: "Two", isChecked: true },
{ id: 3, name: "Three", isChecked: false }
]);
const [clicked, setClicked] = useState([]);
const clickData = index => {
let tempData = data.map(res => {
if (res.id !== index) {
return res;
}
res.isChecked = !res.isChecked;
return res;
});
setClicked(tempData.filter(res => res.isChecked));
};
useEffect(() => {
setClicked(data.filter(res => res.isChecked));
}, []);
return (
<div>
{data.map((res, i) => (
<div key={i}>
<input
type="checkbox"
checked={res.isChecked}
key={i}
onChange={() => {
clickData(res.id);
}}
/>
<label>{res.name}</label>
</div>
))}
{clicked.map(({ name }, i) => (
<p key={i}>{name}</p>
))}
</div>
);
}
https://stackblitz.com/edit/react-y4fdzm?file=src/App.js
Supposing you're iterating through your data in a similar fashion:
{data.map((obj, index) => <div key={index} onClick={handleClick}>{obj.name}</div>}
You can add a data attribute where you assign the checked value for that element, so something like this:
{data.map((obj, index) => <div key={index} data-checked={obj.isChecked} data-index={index} onClick={handleClick}>{obj.name}</div>}
From this, you can now update your isClicked state when the handleClick function gets called, as such:
const handleClick = (event) => {
event.preventDefault()
const checked = event.target.getAttribute("data-checked")
const index = event.target.getAttribute("data-index")
// everytime one of the elements get clicked, it gets added to isClicked array state if true
If (checked) {
let tempArr = [ ...isClicked ]
tempArr[index] = checked
setClicked(tempArr)
}
}
That will let you add the items to your array one by one whenever they get clicked, but if you want all your truthy values to be added in a single click, then you simply need to write your handleClick as followed:
const handleClick = (event) => {
event.preventDefault()
// filter data objects selecting only the ones with isChecked property on true
setClicked(data.filter(obj => obj.isChecked))
}
My apologies in case the indentation is a bit off as I've been typing from the phone. Hope this helps!