Updating object with useState Hook after rendering text input - javascript

I'm in the early process of building a commissioning checklist application for our company. Since the checklist is fairly large (and many of them) I wanted to create a function that maps through an object and after rendering the values written would update the appropriate states with a useState Hook.
The page is rendering without any issues. The problem only appears once the input is changed. Instead of updating the correct state in the object. It seems the logic is adding an additional section in my object and creating another input element.
import React, { useState } from 'react'
const ProjectInfo = () => {
const _renderObject= () => {
return Object.keys(answers).map((obj, i) => {
return(
<div key={obj}>
<label>{answers[obj].question}</label>
<input type="text" onChange={(e, obj) => setAnswer(
{
...answers,
obj:{
value: e.target.value
}})} />
</div>
)
})
}
const [answers, setAnswer] = useState({
11:{
question:"Project Name",
value:""
},
12:{
question:"Project Number",
value:""
}
})
return(
<div>
<section>
{_renderObject()}
</section>
<p>{`Project Number is: ${answers[11].value}`}</p>
<p>{`Project Name is: ${answers[12].value}`}</p>
</div>
)
}
export default ProjectInfo
I was expecting for the state to just update as normal. But what I'm suspecting is in my renderObject method my obj variable for my .map function is not being used inside my setAnswer function and causes another field to be created with a key name of "obj".
If this is the issue is it possible to have the setAnswer function in my renderObject Method to use the "obj" value of the map function and not the actual value of the word obj as key?
If not what would be the best way to approach this? I was thinking of adding a submit button at the bottom of the screen and updating all states with an onClick event listener. But now I'm think I'll have the same issue since the scope of the obj variable isn't resolved.
Any help would be greatly appreciated. I've only been doing this for a couple of months, any advice and feedback would also be appreciated!

You seem to be not using the dynamic key correctly while updating state. Also you need to update the value within the key and not override it. Also obj shouldn't be the second argument to onChange instead it must be received from the enclosing scope
const _renderObject= () => {
return Object.keys(answers).map((obj, i) => {
return(
<div key={obj}>
<label>{answers[obj].question}</label>
<input type="text" onChange={(e) => setAnswer(
{
...answers,
[obj]:{
...answers[obj],
value: e.target.value
}})} />
</div>
)
})

onChange={(e, obj) => setAnswer(
{
...answers,
obj:{
value: e.target.value
}})}
Here you spreading answers and add another object with the target value. that is the issue. Hope you understand the point.
TRY THIS
onChange={
(e, obj) => {
const updatedAnswer = answer.map(ans => ans.question === obj.question ? {...ans,value: e.target.value }:ans)
setAnswer(
{
...updatedAnswer
}
)
}
}
BW your object should contain propper ID for the key.

Its because you are not updating the keys correctly and you need to pass obj in input onchange callback as it make another reference, not the mapped array(obj). So in your case that obj is undefined. Here is working code :
const _renderObject = () => {
return Object.keys(answers).map((obj, i) => {
return (
<div key={obj}>
<label>{answers[obj].question}</label>
<input
type="text"
onChange={e =>
setAnswer({
...answers,
[obj]: { //take obj
...answers[obj],//keep specific object question
value: e.target.value//change only specfic object value
}
})
}
/>
</div>
);
});
};
Here is working url: https://codesandbox.io/s/hardcore-pike-s2hfx

Related

ReactJS - As can I generate several form fields with value properties sharing the same names but not the same values?

After 2 days of fierce struggles, I decided to post for the first time on Stack Overflow :).
My concern is the following:
As part of a project, I want to generate a number of input fields equal to the size of an array.
Example :
function App() {
const [UseState, setUseState] = useState("");
const array = [0, 1, 2];
const HandleAdding = (e) => {
e.preventDefault;
const returnJson = { UseState };
//rest of the code
};
return (
<div>
{array.map(() => (
<form onSubmit={HandleAdding}>
<input value={UseState} onChange={(e) => setUseState(e.target.value)} />
</form>
))}
</div>
);
}
We can already know the problem, when inserting a value on a field, this same value will be inserted on all the other fields which share the same UseState as value.
For even more concrete, here is an illustration of the problem on CodeSandBox:
https://codesandbox.io/s/adoring-rgb-n0bdk?file=/src/App.js
So try to fill in a field and you will notice the problem. I tried a lot of things to solve my problem like this:
https://codesandbox.io/s/blissful-rain-mcol2?file=/src/App.js (I'm trying to adapt the code in order to solve my problem).
I do not put anything more to prevent it from becoming too long.
So, would you have a solution to solve the problem?
For my part I am short of ideas, and still a beginner on React.
Thanks in advance! ^^
function App() {
const [UseState, setUseState] = useState({});
const array = [0, 1, 2];
const size = array.length;
const HandleAdding = (e) => {
e.preventDefault;
const returnJson = { UseState };
//rest of the code
};
return (
<div>
{array.map((x) => (
<form onSubmit={HandleAdding}>
<input value={UseState[x]} onChange={(e) => setUseState({...UseState, [x]: e.target.value})} />
</form>
))}
</div>
);
}
You can easily solve the problem by changing the structure of UseState to become an object with the pattern below
UseState = {
0: 'Isaac',
1: 'James',
2: 'John'
}
You need to store multiple value, so when you retrieve for value, use UseState[x], and when you update, you need to spread the object and update the value of a particular properties
Generally, for forms, you hold the values of the inputs in one object in state, and then update that state with the new values when the input has been changed.
At the moment you're creating more forms than you need. You don't even need the form element because the input elements doesn't actually require it.
Initialise your state with an object.
map over the array instead of the length of the array.
Name your inputs so when the values change you can easily update the state.
const { useState } = React;
function Example() {
// Initialise state with an object
const [ data, setData ] = useState({});
// Example input names
const arr = ['name', 'address', 'number'];
// When the input changes, get the name
// and the value, and update the state using
// that information
function handleChange(e) {
const { name, value } = e.target;
setData({ ...data, [name]: value });
}
function handleSubmit() {
console.log(data);
}
function createInputs(arr) {
// `map` over the array and return some JSX
return arr.map(el => {
return (
<label>{el.toUpperCase()}:
<input type="text" name={el} />
</label>
);
});
}
return (
<div onChange={handleChange}>
{createInputs(arr)}
<button onClick={handleSubmit}>Submit</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("react")
);
label { display: block; }
<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"></div>

React Hooks change single value of object with multiple keys without repetition

I'm trying to find a neater way to handle this pattern I keep coming across with react when handling changes for form fields.
For each element of my form object that I handle a change in value for I find myself replicating this pattern quite a bit with the setter function of useState(). I've tried a couple of things like creating shallow copies of the formState and mutating that but the only way I can really get things to work is with the bellow pattern which feels a little repetitive.
const handleTitle = evt => {
props.setFormState({
title: evt.target.value,
bio: props.formState.bio,
formExpertise: props.formState.formExpertise,
formExpertiseYears: props.formState.formExpertiseYears
});
};
If you want to include this.props.formState you can spread the object into the new state. Further, you can use the input’s name as the state key so you don’t have to rewrite this for every input:
props.setFormState({
...this.props.formState, // copy props.formState in
[evt.target.name]: evt.target.value // use input name as state key
});
Suggestion:
You might consider moving the state merging up into the parent component:
// parent component
const [formState, setFormState] = React.useState({});
const onFieldChange = (field, value) => {
setFormState({
...formState,
[field]: value
});
}
return (
<MyFormComponent
formState={formState}
onFieldChange={onFieldChange}
/>
);
Each input can then invoke onFieldChange with the field name and value without concerning itself with the rest of the state:
function MyFormComponent ({onFieldChange}) {
const handler = ({target: {name, value}}) => onFieldChange(name, value);
return (
<div>
<input name="title" value={formState.title} onChange={handler} />
<input name="bio" value={formState.bio} onChange={handler} />
<input name="expertise" value={formState.expertise} onChange={handler} />
</div>
);
}

What's the best way to ensure no null values

This question may be more about opinion than fact, but I'm unsure so thought I'd ask.
I'm building some forms which will display data and allow edits, the field data comes from props (as a parent component is using a GraphQL query to pull a larger amount and pass to each child).
I'm finding some input data is evaluating to null (as it's not passed back from the query) which throws a warning as inputs don't like being assigned null values.
My question is, when passing these values, what's the cleanest way to run checks on each variable and assign an empty string if needed?
So far the two options i've tried are:
Conditionally assign each to the state object, but this feels clunky and is a lot of code:
const [state, setState] = useState({
telephone: props.telephone ? props.telephone : '',
nickname: props.nickname ? props.nickname : ''
etc...
});
Or to define a function which maps over props and checks values, before setting state:
useEffect( () => {
let state_arr = {};
Object.keys(props).map( (key) => {
if( !props[key] ) state_arr[key] = '';
else state_arr[key] = props[key];
} );
setState(state_arr);
}, [] )
Honestly this feels cleaner than the first option, but there are a number of places this will occur and to have to do this in each feels counter productive.
Any help/insight appreciated.
EDIT: It turns out OP is using Material UI for this..Meaning, the reason the input is showing a warning is due to Material UI using PropTypes. I suggested that OP create a wrapper for the <Input /> component and pass through all props. Inside of the wrapper component you can just do: <InputWrapper value={props.value || ""} {...rest} /> and this covers things..
Live Demo
InputWrapper:
import React from 'react';
import { Input } from '#material-ui/core';
export default function InputWrapper({ value, ...rest }) {
return <Input value={value || ""} {...rest} />
}
InputWrapper In Use:
import React, { useState, useEffect } from 'react';
import { render } from 'react-dom';
import InputWrapper from './InputWrapper.js';
function App(props) {
const [state, setState] = useState({});
useEffect(() => {
setState({
name: props.name,
age: props.age,
hairColor: props.hairColor,
})
}, [props.name, props.age, props.hairColor]);
const handleChange = (event, inputType) => {
setState({...state, [inputType]: event.target.value})
}
return(
<div>
{/* Shows that you can pass through native <Input /> props: */}
{/* state.name is null here! Warning is NOT thrown in the console! */}
<InputWrapper value={state.name} fullWidth onChange={e => setState({...state, name: e.target.value})} />
<InputWrapper value={state.name} multiline onChange={e => setState({...state, name: e.target.value})} />
{Object.keys(state).map((item, index) => {
return (
<div>
<InputWrapper
key={`${item}_${index}`}
value={state[item]}
onChange={e => handleChange(e, item)} />
</div>
);
})}
</div>
);
}
render(
<App name={null} age={44} hairColor="blue" />,
document.getElementById('root')
);
ORIGINAL ANSWER:
What is your use case? There is no reason to run checks and assign empty strings...
If you are trying to enforce that certain properties are used, please look into PropTypes... If you are not wanting to enforce that certain props get used, I would recommend checking for a value during use of the variable. Even if you set it to an empty string initially, you could still encounter errors down the line - I don't understand what you gain from an empty string.
I don't understand the use case - can you elaborate more on why you need to set it to an empty string?
If you really wanted to, you could verify like: useState({two: props.two || ""}) ...but it is still unnecessary..
// Notice how prop "two" is not being used..
function Test(props) {
const [state, setState] = React.useState({
one: props.one,
two: props.two
})
return(
<div>
<p>{state.one}</p>
<p>Even though <code>state.two</code> does not exist, there are no errors.. (at least for this demonstration)</p>
<input type="text" value={state.two} />
<input type="text" value={state.two} defaultValue={"default"} />
<p><i>If you really wanted to, you could verify like:</i><code>useState({two: props.two || ""})</code><i>...but it is still unnecessary..</i></p>
</div>
);
}
ReactDOM.render(<Test one="I AM ONE" />, document.body)
code {
margin: 0 10px;
padding: 3px;
color: red;
background-color: lightgray;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
What about making method KickOutNullValues() which will do what you want and then you can reuse it everywhere you need. That would be more elegant.
This is a tough question, i don't know the right answer. You already tried two ways, the different way that I normally do is,
If you just want to get the display right, i would just do
<Telephone data={props.telephone} />,
const Telephone = ({data}) => { if (!data) return null }
I found this is to allow the child component to ensure the validity of this issue rather than sorting out the data in the parent API level.
Telephone.defaultProps = {
data: ''
}
This further ensures that if the data is null, it'll be reset to '' by the defaultProps
The reason I prefer this way most of time is that I don't really want to mess with the origin TRUTH of the API data.
Of course your ways might be better if you do want to ensure the data is valid at all time :)
Your code will start to have spaghetti-like qualities if you put the raw algorithm inside your callback. I recommend writing a function outside.
Your usage of Array#map is not correct, or rather you are using it in an unintended way. Array#map is used to construct an entirely new array. You are simulating Array#forEach. Also, you're performing a falsey conditional check. null is one of many values that are considered false in JavaScript. Namely, your pain points will probably be undefined, 0, and ''. If the only invalid return value is null, then check for null explicitly.
The enumerable that is for your intended use case is Array#reduce:
function nullValueReplacer(obj) {
return Object.entries(obj).reduce((newStateArr, [currentKey, currentValue]) => {
if (currentValue === null) {
newStateArr[currentKey] = ''
} else {
newStateArr[currentKey] = currentValue
}
return newStateArr
}, {});
}
As a side note, you might want to update your variable names. It's pretty deceptive that you have a variable called state_arr that is an object.
Array of objects - little fix
You should not use key with map..
think about this: (similar to yours)
useEffect(() => {
let state_arr = Object.keys(props).map(prop => prop ? {prop} : { prop: '' });
setState(state_arr);
}, [])
By using this code you make an array with object and have easy access for every item
In case there is no nickname it will look like that:
[{ telephone: '245-4225-288' }, { nickname: '' }]
What do you think?

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?

ReactJS: lifting the parameter several levels up?

I want to upstream the key of the component on change. I.e., I write
onChange= {(e) => this.props.onChange(e, key)}
However, in my case this.props.onChange also points in its turn to its props.onChange function. I've tried to define all the hierarchically "upper" assignments as onChange={this.props.onChange} as well as onChange={(e,key) => this.props.onChange(e,key)}, but nothing helps the fact that key is undefined when it comes to the actual function body.
Here's the full code of that what has to do with the peoblem:
handleValueChange = (e, key) => {
console.log('Event was on ' + key);
let currElemData = this.state.currentElementsData.slice();
currElemData[key] = {value: e.target.value};
this.setState({currentElementsData: currElemData});
};
addFilter = () => {
let activeFiltersNow = this.state.activeFilterElements.slice();
activeFiltersNow.push(<Filter key={this.state.filterCounter} onSelect={(e, key) => this.handleReferenceSelection(e, key)}
onChange={(e,key) =>this.handleValueChange(e, key)}/>);
let currElemData = this.state.currentElementsData.slice();
currElemData.push(InitialHelper.getInitialFilterData());
this.setState({activeFilterElements: activeFiltersNow, currentElementsData: currElemData, filterCounter: this.state.filterCounter++});
};
...
class Filter extends Component {
render() {
return <div className="filterContainer">
{this.props.isFirst? '':<FilterPrefix />}
<FilterReference onSelect={(e,key) => this.props.onSelect(e,key)} key={this.props.key}/>
<FilterAssignment />
<FilterValue onChange={(e,key) => this.props.onChange(e,key)} key={this.props.key}/>
<DeleteFilterButton />
</div>
}
}
...
class FilterValue extends Component{
render() {
return <input onChange={(e) => this.props.onChange(e,this.props.key)} key={this.props.key}/>
}
loadSuggestions(reference) {
}
}
I'm not quite sure what you are trying to do here, but I've noticed that you are falling for a pretty common ReactJs pitfall which is that:
Keys serve as a hint to React but they don't get passed to your
components. If you need the same value in your component, pass it
explicitly as a prop with a different name
meaning that this.props.key will always be undefined. So, I'm afraid that all the instances in where you are using this.props.key are not going to behave as you expect...

Categories