How can I create an array of states in React using hooks? - javascript

I have a number, n, that can be any value and I want to render n input fields while keeping track of each input's state but I'm having trouble figuring out how. For example, if n = 3, I want to render something like this:
<div>
<input onChange={(e) => setValue1(e.target.value)}/>
<input onChange={(e) => setValue2(e.target.value)}/>
<input onChange={(e) => setValue3(e.target.value)}/>
< /div>
In this example, I would manually need to create three states: value1, value2, value3. My goal is to have it dynamic so if in the future I change n to 4 or any other number, I don't have to manually create more states and mess with the component. Is there a good way to accomplish this using hooks?

You have to create a inputs state in order to track every input:
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [inputs, setInputs] = useState(Array(10).fill(''));
const inputChangedHandler = (e, index) => {
const inputsUpdated = inputs.map((input, i) => {
if (i === index) {
return e.target.value;
} else {
return input;
}
});
setInputs(inputsUpdated);
};
return (
<div>
{inputs.map((input, i) => (
<input onChange={e => inputChangedHandler(e, i)} value={input} />
))}
</div>
);
}
You can check here, how things work:
https://stackblitz.com/edit/react-sdzoqh

You can create a new array with useState hook of size num that is passed from its parent and then using its index i.e. i you can change its input value using setValue function.
CODESANDBOX DEMO
Just for DEMO purpose and make the input come to a new line so I've wrapped it into div.
export default function App({ num }) {
const [arr, setValue] = useState(Array(num).fill(""));
console.log(arr);
function onInputChange(index, event) {
console.log(event.target.value);
setValue((os) => {
const temp = [...os];
temp[index] = event.target.value;
return temp;
});
}
return (
<div className="App">
{arr.map((n, i) => {
return (
<div key={i}>
<input onChange={(e) => onInputChange(i, e)} value={n} />
</div>
);
})}
</div>
);
}

Maybe I would create a custom hook to generate my inputs like
import React, { useState } from "react";
const CreateInput = (n) => {
const array = new Array(n).fill("");
const [valueInput, setValueInput] = useState({});
const handleChange = (event) => {
const { name, value } = event.target;
setValueInput({
...valueInput,
[name]: value,
});
};
const Input = array.map((_, i) => (
<input key={i} name={i} onChange={handleChange} />
));
return {
Input,
};
};
const Inputs = () => {
const { Input } = CreateInput(3);
console.log(Input);
return <div>{Input}</div>;
};
export default Inputs;

This could be done with an array in the state, with the values in the inputs. Initialize with empty strings
const [values, setValues] = useState(Array(n).fill(""))
const handleChange = (e, i) => {
const copy = values;
copy[i] = e.target.value
setValues(copy)
}
return (
<div>
{Array(n).map((x,i) => (
<input value={values[i]} onChange={e => handleChange(e,i)} />
))}
</div>
)

you can use useState([]) with array as default value for example
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const n = 3;
const [values, setValues] = useState(new Array(n).fill(1, 0, n));
const handleChange = (i, value) => {
const v = [...values];
v[i] = value;
setValues(v);
};
const inputsRendrer = (values) => {
return values.map((v, i) => {
return (
<input
key={i}
value={values[i]}
onChange={(event) => handleChange(i, event.target.value)}
/>
);
});
};
return <div className="App">{inputsRendrer(values)}</div>;
}
new Array(n).fill(1, 0, n) // this create new array with values of 1 and length of n`

Related

How to set array of values to state and fetch during submit in React

Depending on the count, the number of text box will be generated.
When I try to set the text box value to the state, textbox is not accepting more than one value.
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [option, setOption] = useState([]);
const Generator = (count) => {
let textArr = [];
for (let i = 0; i < count.count; i++) {
let name = "txt" + i;
textArr.push(<input type="text" key= {name} id={name} onChange={e => onChangeOptions(e,i)} value={getOptionValue(i)}/>);
}
return textArr;
};
const getOptionValue = (i) => {
let opt = [...option];
return opt[i];
}
const onChangeOptions = (e,i) => {
let val = e.target.value;
let opt = [...option];
opt[i] = val;
setOption(opt);
console.log(opt);
}
const onSubmitForm = (e) => {
console.log(option);
}
return (
<div>
<form onSubmit={onSubmitForm}>
<input
type="text"
id="count"
onChange={(e) => setCount(e.target.value)}
/>
<br />
<Generator count={count} />
<button>Submit</button>
</form>
</div>
);
}
Generator is defined inside your component, so it's a different function on each render. React thinks a different element is being rendered each time, which makes the input lose focus. Instead, directly create the array of inputs in the JSX with Array#map:
{
[...Array(+count)].map((_, i) => (
<input type="text" key={"txt" + i} id={"txt" + i} onChange={e => onChangeOptions(e,i)} value={getOptionValue(i)}/>
))
}
Alternatively, extract Generator into a separate component. Do not define it inside another component.

React Hooks: state variable having wrong value in event handlers, not able to type in input

Below, i am rendering <App/> component with children as <Input/> component array. I added few inputs using "add new" button. I am able to add input text components. But, when i am typing value in text, it is not displaying. i am not able to modify object in state array since index is showing as "-1" in setData function. Due to this, value is not showing when we type in text box. Please let me know why state is [] when i am accessing in setData function.
function Input(props)
{
return (
<div>
<label htmlFor='variable'>Name</label>
<input id='variable'
type='text'
value={props.value}
onChange={(e) => props.setData(props.id, e.target.value)} />
</div>
)
}
function App()
{
let [state, setState] = React.useState([])
let [inputs, setInputs] = React.useState([])
let setData = ((id, value) =>
{
console.log(state); // prints []
let index = state.findIndex(ele => ele.key === id);
console.log(index); // prints -1
if (!(index === -1))
{
setState(state =>
{
state[idx]["value"] = value;
})
}
})
let handleAdd = () =>
{
let idx = `${new Date().getTime()}`
let tempState = {
"key": idx,
"value": "",
}
setState(state => [...state, tempState])
let input = <Input key={tempState.key}
value={tempState.value}
id={tempState.key}
setData={setData} />
setInputs(inputs => [...inputs, input])
}
return (
<div>
<button onClick={handleAdd}>add new</button>
<div>
{inputs}
</div>
</div>
)
}
When you create an Input component inside handleAdd, it creates a closure and as a result setData gets the state that existed when the component was created, missing the newly added state.
In general, creating components and saving them to state is not a good approach. Instead it's better to only save the data onto state and render the components based on it.
Here's one way to do this, note how much simpler the component and its logic are.
function App() {
let [state, setState] = React.useState([]);
let setData = (id, value) => {
const newState = state.map((st) => {
if (st.key === id) {
st.value = value;
}
return st;
});
setState(newState);
};
const addInput = () => {
const idx = `${new Date().getTime()}`;
setState([...state, { key: idx, value: '' }]);
};
return (
<div>
<button onClick={addInput}>add new</button>
<div>
{state.map((st) => (
<Input value={st.value} key={st.key} setData={setData} id={st.key} />
))}
</div>
</div>
);
}

How to dynamically push added inputs value to array?

I have React component where I can dynamically add new text inputs. So, I need to push the values from the inputs to array.
Can anyone help me how to do this?
Here is my code:
function FormPage({ setData }) {
const [item, setItem] = useState([]);
const [counter, setCounter] = useState(0);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (setItem) => setItem((ing) => [...ing, newItem]);
return (
{Array.from(Array(counter)).map((c, index) =>
<TextField
key={index}
label="Item"
onChange={() => setItem(i=> [...i, (this.value)])}
/>
)}
<Button onClick={handleCounter}>Add one more item</Button>
)
}
Here is example in sandbox:
https://codesandbox.io/s/solitary-sound-t2cfy?file=/src/App.js
Firstly, you are using two-way data binding with your TextField component, so you also need to pass a value prop.
Secondly, to get the current value of TextField, we don't use this.value. Rather, the callback to onChange takes an argument of type Event and you can access the current value as follows
<TextField
...
onChange={(e) => {
const value = e.target.value;
// Do something with value
}}
/>
You cannot return multiple children from a component without wrapping them by single component. You are simply returning multiple TextField components at the same level, which is also causing an error. Try wrapping them in React.Fragment as follows
...
return (
<React.Fragment>
{/* Here you can return multiple sibling components*/}
</React.Fragment>
);
You are mapping the TextField components using counter which is equal to the length of item array. In handleCounter, we'll add a placeholder string to accomodate the new TextField value.
...
const handleCounter = () => {
setCounter(prev => prev+1); // Increment the counter
setItem(prev => [...prev, ""]); // Add a new value placeholder for the newly added TextField
}
return (
<React.Fragment>
{ /* Render only when the value of counter and length of item array are the same */
counter === item.length && (Array.from(Array(counter).keys()).map((idx) => (
<TextField
key={idx}
value={item[idx]}
label="Item"
onChange={(e) => {
const val = e.target.value;
setItem(prev => {
const nprev = [...prev]
nprev[idx] = val;
return nprev;
})
}}
/>
)))}
<br />
<Button onClick={handleCounter}>Add one more item</Button>
</React.Fragment>
);
Here is the sandbox link
Try this:
import "./styles.css";
import React, { useState } from "react";
export default function App() {
// Changes made here
const [item, setItem] = useState({});
const [counter, setCounter] = useState(0);
console.log("item 1:", item[0], "item 2:", item[1],item);
const handleCounter = () => {
setCounter(counter + 1);
};
const addItem = (newItem) => setItem((ing) => [...ing, newItem]);
return (
<>
{Array.from(Array(counter)).map((c, index) => (
<input
type="text"
key={index}
//Changes made here
value={item[index]}
label="Item"
// Changes made here
onChange={(event) => setItem({...item, [index]:event.target.value })}
/>
))}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
Instead of using an array to store the input values I recommend using an object as it's more straight-forward.
If you wanted to use an array you can replace the onChange event with the following:
onChange={(event) => {
const clonedArray = item.slice()
clonedArray[index] = event.target.value
setItem(clonedArray)
}}
It's slightly more convoluted and probably slightly less optimal, hence why I recommend using an object.
If you want to loop through the object later you can just use Object.entries() like so:
[...Object.entries(item)].map(([key, value]) => {console.log(key, value)})
Here's the documentation for Object.entries().
codeSolution: https://codesandbox.io/s/snowy-cache-dlnku?file=/src/App.js
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [item, setItem] = useState(["a", "b"]);
const handleCounter = () => {
console.log(item, "item");
setItem([...item, ""]);
};
const setInput = (index) => (evt) => {
item.splice(index, 1, evt.target.value);
setItem([...item]);
};
return (
<>
{item.map((c, index) => {
return (
<input
type="text"
key={index}
label="Item"
value={c}
onChange={setInput(index)}
/>
);
})}
<button onClick={handleCounter}>Add one more item</button>
</>
);
}
I have solved for you . check if this works for you , if any issues tell me

Handling an indetermined amount of states for n checkboxes in React

I have this solution for handling a fix number of states for a fix number of checkboxes:
import { useState } from 'react';
function App() {
const [arrValues, setArrValues] = useState(
// use a function here to only create the initial array on mount
() => Array.from(
{ length: 10 }
)
);
const setcheckBoxValue = (i) => {
setArrValues(
arrValues.map((v, j) => j !== i ? v : !v)
);
console.log(arrValues);
}
return (
<div className="App">
{arrValues.map( (val, i) =>
<input
key={i}
type="checkbox"
checked={val}
onChange={() => setcheckBoxValue(i)}
>
</input>)
}
</div>
);
}
export default App;
In the code it is 10, but, what if the length of the array is not known until I read some value in a database,
Rafael
You can do something like this. The example below is using an object rather than an array, but this is just example. Also, the “initialValues” could be easily generated from data (from database). These could be the undetermined values you mentioned.
const MyTest = () => {
const initialValues = {
values: { checkbox1: true, checkbox2: false, checkbox3: true }
}
const [state, setState] = useState(initialValues || {})
const handleChange = (event) => {
const target = event.target
const value = target.checked
const name = target.name
setState((prevState) => ({
values: {
...prevState.values,
[name]: value
}
}))
}
return (
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
{state.values &&
Object.keys(state.values).map((checkboxKey, i) => (
<input
name={checkboxKey}
key={i}
type="checkbox"
checked={state.values[checkboxKey]}
onChange={handleChange}
/>
))}
</div>
)
}

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

Categories