Let's say I have a <SelectPicker/> component where I can select an option. What I want is how to add another <SelectPicker/> after I selected an option.
function DynamicComponent() {
const [state, setState] = useState([
{ name: null, id: '1' },
]);
const handleAdd = (value) => {
// Updating logic
};
return(
<>
{ state.map(item => {
return <SelectPicker
onSelect={handleAdd}
key={item.id}
value={item.name}
data={options} />
})
}
</>
);
}
In the example above, let's say there is default SelectPicker which is not selected. After selection, I think handleAdd function should update object that has id equal to '1' and add another object like this { name: null, id: '2' }.
What is the best way to achieve such functionality in react? Any help would be appreciated.
Thank you.
On an abstract level, what you want to do is have an array of components inside your state which is then called by the Render function of DynamicComponent. This array should get its first SelectPicker component immediately, and every time handleAdd is called you add a new SelectPicker to the array using your setState function. You can get the id for each new SelectPicker component by finding array.length.
In addition to the question, the below note from OP is also addressed in the below question
what if I want to update object's name property that has id:1 and add
new object to state at the same time?
This may be one possible solution to achieve the desired result:
function DynamicComponent() {
const [myState, setMyState] = useState([
{name: null, id: '1'}
]);
const handleAdd = arrIdx => setMyState(prev => {
const newArr = [...prev];
prev[arrIdx]?.name = ">>>>----new name goes here---<<<<";
return [
...newArr,
{
name: null,
id: (prev.length + 1).toString()
}
]
});
return(
<div>
{myState.map((item, idx) => (
<SelectPicker
onSelect={() => handleAdd(idx)}
key={item.id}
value={item.name}
data={options}
/>
)}
</div>
);
}
NOTES
Avoid using variable-names such as "state"
Passed the "index" from the .map() iteration
This helps in tracking the exact array-element
The new element with name: null is added as the last
The id is calculated by incrementing the current length by 1
Related
Short Explanation
I just want to get the data from the textboxes and send them to the server. In jquery I would just list out all my textboxes and get the value and form the json and send it to the server
Please see example code
https://codesandbox.io/s/xenodochial-euclid-3bl7tc
const EMPTY_VALUE = "";
const App = () => {
const [valueFirstName, setValueFirstName] = React.useState(EMPTY_VALUE);
const [valueMiddleName, setValueMiddleName] = React.useState(EMPTY_VALUE);
const [valueLastName, setValueLastName] = React.useState(EMPTY_VALUE);
return (
<div>
First Name:
<Work365TextBox value={valueFirstName} setValue={setValueFirstName} />
<div>{valueFirstName}</div>
Middle Name:
<Work365TextBox value={valueMiddleName} setValue={setValueMiddleName} />
<div>{valueMiddleName}</div>
Last Name:
<Work365TextBox value={valueLastName} setValue={setValueLastName} />
<div>{valueLastName}</div>
</div>
);
};
Problem
The current code has a label for first name, middle name, and last name and components to contain the textboxes and then the state of the component is stored in the parent.Then the parent takes the state and displays it below the textbox. So the solution works great. But the code is messy
Question
If I have a form that asks for 20 values what is a cleaner way to handle this ? I have tried to do this with by defining a object as json and calling a method when the value in each textbox changes but then I need to have a method for each textbox I have on the screen and it doesn't seem very clean. How would you solve this problem in a clean way ? Preferably I want to be able to have 50 textbox components but just call one method to update state.
the object solution you mentioned is a great way.
one thing to add is that you can pass a name prop to your input and then in the onChange method you can access it via event.target.name
that way you can dynamically update your object
something like this
const onChangeHandler = (event) => {
const name = event.target.name;
const value = event.target.value;
setState((prev) => ({ ...prev, [name]: value }));
};
another solution is to define it with useReducer but that will need a extra code.
I would just use an array like that:
// one item for each textbox
const textboxes = [
{
// human-readable label
label: "First name",
// unique key for data access
key: "firstName"
},
{
label: "Middle name",
key: "middleName"
},
{
label: "Last name",
key: "lastName"
}
];
A state like this:
const [values, setValues] = React.useState(
// converts array to record-like object
Object.fromEntries(textboxes.map((box) => [box.key, ""]))
);
const handleChange = (key, value) => {
setValues((values) => ({ ...values, [key]: value }));
};
And render it like this:
<div>
{textboxes.map((box) => (
<>
{box.label}:
<Work365TextBox
value={values[box.key]}
setValue={(value) => handleChange(box.key, value)}
/>
<div>{values[box.key]}</div>
</>
))}
</div>
Full example
I need to make a large number of inputs and transfer this data to the server, I decided that the best solution would be to write all the options of these inputs into an array of objects, but I ran into the fact that I can’t get all my inputs to work. help me please
const test = [
{id: 1,state: 'city'},
{id: 2,state: 'language'},
{id: 3,state: 'brand'},
{id: 4,state: 'shop'},
]
const Auth = () => {
const [description, setDescription] = useState({city: "", language: "", brand: "", shop: ""});
const handleClick = async (event: any) => {
await store.update(description.city, description.brand);
};
const update = async (e: ChangeEvent<HTMLInputElement>) => {
setDescription({
...description,
city: e.target.value
});
};
return (
<>
{test.map(({ state, id}) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description.city}
onChange={update}
/>
))}
<Button
className={styles.saveButton}
variant="contained"
color="inherit"
id="login"
onClick={handleClick}
>
Save
</Button>
</>
)
}
You send to TextField description.city for every input. The correct props are like so:
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={update}
/>
See the change in the value prop.
Also, you only update city in the update function. You have to make it so that the update function adapts to what values you pass to it. If you pass the city then it should update the city, if the language then the language and so on.
Overall this is not a good way to implement inputs. I just suggest you do them one by one and send to each TextField its corresponding value and a separate setState for each one.
But just for the sake of the example. The way you can do it is by passing the state value to the Update function.
So your function will look like this:
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription((description) => {
...description,
[state]: e.target.value
});
};
Now you just need to make sure that in the TextField component when you call onChange, you pass to it the event e and state which you have received from props.
Note: If you want to use the value of a state variable in the setState itself, pass to it a callback function like I did in the setDescription
if you want to make it dynamic you would have to send the variable to save to your update method and retrieve your value with description[state]
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e)=>update(e, state)}
/>
const update = async (e: ChangeEvent<HTMLInputElement>, state) => {
setDescription({
...description,
[state]: e.target.value
});
};
I think first and foremost you need your configuration data to try and closely match the elements you're building. So instead of { id, state } use { id, type, name }.
(This may not have a huge effect on your example because you're specifically using a TextField component, but if you were using native HTML controls you could add in different input types like number, email, date etc, and your JSX could deal with it easily.)
Second, as I mentioned in the comments, you don't need for those functions to be async - for example, there's no "after" code in handleClick so there's no need to await anything.
So here's a working example based on your code. Note: I've stripped out the Typescript (because the snippet won't understand the syntax), and the references to the UI components you're using (because I don't know where they're from).
const { useState } = React;
// So, lets pass in out inputs config
function Example({ inputs }) {
// I've called the state "form" here as it's a little
// more meaningful
const [form, setForm] = useState({});
// `handleSave` is no longer `async`, and for the
// purposes of this example just logs the updated
// form state
function handleSave() {
console.log(form);
// store.update(form);
}
// Also no longer `async` `handleChange` destructures
// the name and value from the changed input, and updates
// the form state - a key wrapped with `[]` is a dynamic key
// which means you can use the value of `name` as the key value
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
// In our JSX we destructure out the id, name, and
// type properties from each input object in the config
// and apply them to the various input element properties.
return (
<div>
{inputs.map(input => {
const { id, name, type } = input;
return (
<input
key={id}
type={type}
name={name}
placeholder={name}
value={form[name]}
onChange={handleChange}
/>
);
})}
<button onClick={handleSave}>Save</button>
</div>
);
}
// Our updated config data
const inputs = [
{ id: 1, type: 'text', name: 'city' },
{ id: 2, type: 'text', name: 'language' },
{ id: 3, type: 'text', name: 'brand' },
{ id: 4, type: 'text', name: 'shop' }
];
ReactDOM.render(
<Example inputs={inputs} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Pass both the key that you want to update and the value to the update function:
const update = (key: string, value: string) => {
setDescription({
...description,
[key]: value,
});
};
{test.map(({ state, id }) => (
<TextField
key={id}
label={state}
id={state}
autoComplete="off"
variant="outlined"
className={styles.textFieldAuth}
helperText={state}
value={description[state]}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
update(state, e.target.value)
}
/>
))}
I've made a shorter version of what I'm trying to create for simplicity here. I know some of the code here is wrong. I've spent hours trying different ways and can't get anything to work, so I've stripped it back.
Aim: I'm rendering a FlatList. I need to be able to update the 'qty' in each respective object in the array with the click of a button in each particular FlatList item.
So, if I click 'Increase QTY' in 'abc', then the 'qty' data will be increased by 1.
I've looked everywhere online and can't seem to be able to get any closer. Any help would be hugely appreciated.
import React, { useState } from 'React';
import { View, Text, Button, FlatList } from 'react-native';
const DataApp = () => {
const [data, setData] = useState([
{ id: 1, name: 'abc', qty: 1 },
{ id: 2, name: 'def', qty: 2 },
{ id: 3, name: 'ghi', qty: 3 },
]);
const incQuantityHandler = (data) => {
setData([...data, prevState => qty[prevState] + 1 ])
}
const Item = ({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.qty}</Text>
<Button title="Increase QTY" onPress={incQuantityHandler}/>
</View>
)
const renderItem = ({ item }) => (
<Item name={item.name} qty={item.qty} />
)
return (
<View>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
)
}
export default DataApp;
With your current setup, incQuantityHandler gets called with an event object whenever the button is pressed. You should use onClick by the way.
You can use an arrow function to pass on which button you're pressing, otherwise use a wrapper function:
onClick={() => incQuantityHandler(item.name)}
// or alternatively, but basically the same:
const wrapHandler = item => (() => incQuantityHandler(item.name));
onClick={wrapHandler(item)}
Your incQuantityHandler itself is incorrect. I suggest re-reading the React documentation and learning about array destructuring/spreading, but you probably want something like:
// Remember that now we get the item name instead
const incQuantityHandler = (itemName) => {
// Use an arrow function to mutate data
setData(data =>
// Use map to map over all items
data.map(item => {
// Leave other items the way they are
if (item.name !== itemName) return item;
// Return a modified copy of our target item
// where we changed the qty field
return { ...item, qty: item.qty + 1 };
}));
}
Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l
I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?