Rendering array data with a condition - javascript

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)
}
/>
))}

Related

dynamically add component after select

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

React - How to prevent re-rendering of all the input fields when input changes

I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.

How to update a text field without using the useState hook?

I have a component structured like this:
export const EditProductDescription = ({ product, lang, onChange, onSubmit }: ProductDescriptionInterface) => {
let editedProduct = { ...product };
const handleChange = (field: string, value: string, multiLanguage: boolean) => {
multiLanguage
? { ...editedProduct, description: { [field]: { [lang]: value } } }
: { ...editedProduct, description: { [field]: value } };
console.log(editedProduct);
};
return (
<TextArea
id="description"
placeholder="Description"
value={editedProduct.details.description.text[lang] || ' '}
label="Description"
onChange={e => handleChange('text', e.target.value, true)}
/>
);
};
My Product model ist structured like this:
export class Product {
...
details: {
...
description: {
...
text: {de?: string, en?: string}
...
}
...
}
...
}
I would like to update the description of the product or have the opportunity to edit it.
With my current approach, the product object is not updated as desired. Accordingly, the value in the text field is not updated either.
My handleChange method receives the field I want to edit as a parameter. The multiLanguage parameter is also used to specify whether this should be created in multiple languages. Multilingualism is important here.
How do I have to adapt my handleChange method so that I can get the result I want without using the useState hook? Is that even possible?
What am I doing wrong?

setState only setting last input when using object as state

Im trying to create a form with React. This form uses a custom Input component I created various times. In the parent form Im trying to get a complete object with all names and all values of the form:
{inputName: value, inputName2: value2, inputName3: value3}
For this, I created a 'component updated' hook, that calls the function property onNewValue to send the new value to the parent (two way data binding):
useEffect(() => {
if (onNewValue) onNewValue({ name, value });
}, [value]);
The parent form receives the data in the handleInputChange function:
export default () => {
const [values, setValues] = useState({});
const handleInputChange = ({
name,
value
}: {
name: string;
value: string | number;
}): void => {
console.log("handleInputChange", { name, value }); // All elements are logged here successfully
setValues({ ...values, [name]: value });
};
return (
<>
<form>
<Input
name={"nombre"}
required={true}
label={"Nombre"}
maxLength={30}
onNewValue={handleInputChange}
/>
<Input
name={"apellidos"}
required={true}
label={"Apellidos"}
maxLength={60}
onNewValue={handleInputChange}
/>
<Input
name={"telefono"}
required={true}
label={"Teléfono"}
maxLength={15}
onNewValue={handleInputChange}
/>
<Input
name={"codigoPostal"}
required={true}
label={"Código Postal"}
maxLength={5}
onNewValue={handleInputChange}
type={"number"}
/>
</form>
State of values: {JSON.stringify(values)}
</>
);
};
This way all elements from all inputs should be set on init:
{"codigoPostal":"","telefono":"","apellidos":"","nombre":""}
But for some reason only the last one is being set:
{"codigoPostal":""}
You can find the bug here:
https://codesandbox.io/s/react-typescript-vx5py
Thanks!
The set state process in React is an asynchronous process. Therefore even if the function is called, values has not updated the previous state just yet.
To fix, this you can use the functional version of setState which returns the previous state as it's first argument.
setValues(values=>({ ...values, [name]: value }));
useState() doesn't merge the states unlike this.setState() in a class.
So better off separate the fields into individual states.
const [nombre, setNombre] = useState("")
const [apellidos, setApellidos] = useState("")
// and so on
UPDATE:
Given setValue() is async use previous state during init.
setValues((prevState) => ({ ...prevState, [name]: value }));
The updated and fixed code, look at:
https://codesandbox.io/s/react-typescript-mm7by
look at:
const handleInputChange = ({
name,
value
}: {
name: string;
value: string | number;
}): void => {
console.log("handleInputChange", { name, value });
setValues(prevState => ({ ...prevState, [name]: value }));
};
const [ list, setList ] = useState( [ ] );
correct:
setList ( ( list ) => [ ...list, value ] )
avoid use:
setList( [ ...list, value ] )

Calling setState in callback of setState generate a weird bug that prevents input's value to be updated by onChange

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?

Categories