Updating object in react js in a function based component - javascript

I have this imaginary case where I have a function that has a form, which I can update my data. It is a real estate agency and wants to have option to update its properties when the sellers makes some changes. Note that data might not make much sense irl but it is just for learning purpose.
A property object looks like this:
{
owner: "John doe",
other: {
rooms: 3,
windows: 6
}
address: {
street: "jane doe",
houseNr: 24
}
}
And my component:
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { getProperty } from '../../actions'
const Property = ({ getProperty, property }) => {
const [propertyObj, setPropertyObj] = useState(property);
const { propertyId, owner, address, other } = propertyObj
const propertyAddress = `${address.street}, ${address.houseNr}` // jane doe, 24
const { rooms } = other;
const handleSubmit = (event) => {
event.preventDefault()
// todo: submit the updated property
}
return (
<div className='row align-items-start'>
<div className='col col-md-6'>
<p>Id: {propertyId}</p>
<form>
<div className='row'>
<label className='mt-3'>Owner</label><input className='ml-3' type='text' name='owner' onChange={e => setPropertyObj({ owner: e.target.value })} defaultValue={owner} />
</div>
<div className='row'>
<label className='mt-3 mx-3'>Rooms</label><input className='ml-3' type='text' name='rooms' onChange={e => setPropertyObj({ rooms: e.target.value })} defaultValue={rooms} />
<div className='row'>
<label className='mt-3 mx-3'>Property Address</label><input className='ml-3' type='text' name='propertyAddress' onChange={e => setPropertyObj({ propertyAddress: e.target.value })} defaultValue={propertyAddress} />
</div>
</form>
</div>
<button onClick={(e) => handleSubmit(e)} className='btn btn-success'>Update Property</button>
</div>
)
}
const mapStateToProps = reduxStoreState => {
return {
property: reduxStoreState.property,
}
}
export default connect(mapStateToProps, { getProperty })(Property)
When attempting to update rooms property, I get:
TypeError: other is undefined
But I beliver I would get a similar error when I would try to update the property address.
I know that for property address, I have to split into street and house number to update each separately. The question is how can I update the state variable property so that I can submit that to my function updatePropertyById(id, updatedProperty), I have seen ways to do in cases when it is a class object, but I want to use the new features of react.
How can I update the rooms for instance, without needing to also type the windows, I think the spread operator would be helpful but I just don't know how.

Updating a json object via spread operator
To use the spread operator, you have to know the level of the key. Lets assume we have an object:
const obj = {"field1": {"field2": 1, "field3": 2}, "field4":3}
To update field3 you have to do:
obj = {...obj, field1: {...field1, field3: newValue}}
In your case
This is a little different in your case, since you want to give the key and update the object. One way of doing this is traversing the object
const updateProperty = (obj, key, newVal) => {
let tempObj = {...obj}
Object.keys(tempObj).forEach(k => {
if(k===key) tempObj[k] = newVal
})
setPropertyObj(tempObj)
}

In your code, you've written:
...
onChange={e => setPropertyObj({ rooms: e.target.value })}
...
This overrides the previous value of propertyObj. What you need to do is to keep the previous state using the spread operator:
onChange={e => setPropertyObj(state => ({ ...state, rooms: e.target.value })}
Or even better, you can write a function for this:
function handleChange(e) {
setPropertyObj(state => ({
...state,
[e.target.name]: e.target.value
})
}
...
<input className='ml-3' type='text' name='rooms' onChange={handleChange} defaultValue={rooms} />
...
UPDATE: I think by other, you meant to use the rest syntax in object destructring.
The code { other } = propertyObj means that you're looking for a property with key other inside propertyObj. If you want to assign the remaining properties to a newly defined variable, you should prepend it with three dots:
const { propertyId, owner, address, ...other } = propertyObj

Related

Reset controlled value of single individual input in form upon button click

My app gets initialized with a json payload of 'original values' for attributes in a task form. There is a shared state between various components through a context manager submissionState that also gets imported to this form component. This shared state is a copy of the original values json but includes any edits made to attributes in the payload. I would like to include individual 'reset' buttons for each input of the form which would update the shared state to the original value in the json payload. The original values get passed into the parent form component as props and the edited value state gets called from within the parent component as well.
const FormInput = ({ fieldName, fieldValue, onChange, originalValue }) => {
const handleReset = e => {
//????
}
return (
<div>
<label htmlFor={fieldName}>
{fieldName}
</label>
<br />
<input type="text"
name={fieldName}
value={fieldValue}
onChange={onChange}/>
<button id="reset" onClick={handleReset}>↻</button>
<br />
<div>{originalValue}</div>
</div>
);
};
const TaskForm = (payload: TaskPayload) => {
const { submissionState, setSubmission } = useTask();
function handleChange(evt) {
const value = evt.target.value;
setSubmission({
...submissionState,
[evt.target.name]: value,
});
}
const taskFields = ["name", "address", "address_extended", "postcode", "locality", "region", "website"];
return (
<div>
<form>
{taskFields.map((field) => {
<FormInput
key={field}
fieldName={field}
fieldValue={submissionState[field]}
onChange={handleChange}
originalValue={payload[field]}
/>
})
}
</form>
</div>
);
};
export default TaskForm;
What I would like to do is include logic in the reset button function so that any edits which were made in a form input (from state) get reverted to the original value (stateless), which comes from the payload props: payload[field].
The form input is controlled through a global shared state submissionState, so the reset button logic can either modify the shared state itself with something like:
const handleReset = (submissionState,setSubmissionState) => {
setSubmission({
...submissionState,
fieldName: originalValue,
});
but I would need to pass the submissionState and setSubmission down through to the child component. It would be better if I can somehow update the value attribute in the input, which in-turn should potentially update the shared state? And the logic can just be something like this (assuming I can somehow access the input's value state in the reset button)
const handleReset = (?) => {
/*psuedo code:
setInputValueState(originalValue)
*/
}
I would highly recommend using react-hook-form if it's an option. I've implemented it across several projects and it has never let me down. If it's just not possible to use a library, then keep in mind that React is usually unidirectional. Don't try to work around it, since it works that way by design for most cases you can encounter. Otherwise…
const TaskForm = (payload: TaskPayload) => {
const { submissionState, setSubmission } = useTask();
const upsertSubmission = (upsert) =>
setSubmission({
...submissionState,
...upsert,
});
const handleChange = ({ target }) => {
upsertSubmission({
[target.name]: target.value,
});
};
const reset =
(originalValue) =>
({ target }) => {
upsertSubmission({
[target.name]: originalValue,
});
};
/* Also something like this. RHF will handle most of this for you!
* const reset = (originalValue, fieldName) =>
* upsertSubmission({[fieldName]: originalValue})
*/
const taskFields = [];
return (
<div>
<form>
{taskFields.map((field) => (
<FormInput
key={field}
fieldName={field}
onChange={handleChange}
reset={reset(originalValue)}
value={submissionState[field]}
/>
))}
</form>
</div>
);
};

React Hooks: State is resetting to empty array even if I use the spread operator, prevState, etc

Simplified Code Sample right here
WORDS:
In short: My items state is resetting to [] with each NEW checkbox clicked and I dont understand why. But instead I want to use the spread operator and useState hooks to push an new item into the array so it's an array of objects.
Current behavior in detail: I'm creating an object and setting it in state using all (and I mean ALL) manner of React useState hooks like this: setItems((prevState) => [...prevState, { [evt.target.value]: evt.target.checked }]); As I check one item it's added and items becomes an array of objects (it being added over and over again is not the problem; I'll add a check for that later). BUT Here's the problem: when I click a NEW checkbox the items array is set back to [] and isnt concatenated with the prev items—even though I'm using prevState, spread operator, an arrow func as a wrapper, and all that jazz.
Desired behavior: Every time I check a checkbox, I want to update items [] to push a new object into it, which represents all items that have ever been checked. Before you say anything about duplicating: I'll add the check to see if an item is already in the array, and just update it if so. And before I add all items to cart, I'll strip all objects with checked = false states.
Can you help me understand what react lifecycle fundamentals I'm missing here; why is this happening? And how can I fix it?
CODE:
Where this is happening:
Simplified version of InputComponent
const InputComponent = ({ type, itemId, handleSearchQuery, onSubmit }) => {
const [items, setItems] = useState([]);
const captureInput = (evt) => {
if (evt.target.type === 'checkbox') {
setItems((prevState) => [...prevState, { [evt.target.value]: evt.target.checked }]);
}
};
const renderCheckbox = () => {
return (
<form>
<input type={type} name={itemId} value={itemId} onChange={setItem} />
<input name={itemId} type='submit' value='Add to Cart' />
</form>
);
};
return (
<div className='input-bar'>
{renderCheckbox()}
</div>
);
};
export default InputComponent;
Where this component is used:
import React from 'react';
import InputComponent from './InputComponent';
import './ResultsRenderer.css';
function ResultsRenderer({ data }) {
const renderListings = () => {
let listings = data ? data.Search : null;
return listings
? listings.map((item) => {
return (
<div className='cart-row'>
<InputComponent type='checkbox' className='cart-checkbox' itemId={item.imdbID} />
<div key={item.imdbID} className={item.imdbID}>
<img src={`${item.Poster}`} alt={item.Title} />
<div>
Title<em>{item.Title}</em>
</div>
<div>{item.Year}</div>
<em>{item.imdbID}</em>
</div>
</div>
);
})
: null;
};
return <>{renderListings()}</>;
}
export default ResultsRenderer;
items state is doing its job perfectly fine, you misunderstood the situation.
you're using items state inside InputComponent and for each listings item there is one InputComponent and each one have their own items, I think you meant to use items state inside ResultsRenderer Component to chase all selected items.
here is the changes you need to do:
const InputComponent = ({ type, itemId, setItems }) => {
const captureInput = (evt) => {
if (evt.target.type === "checkbox") {
setItems((prevState) => [
...prevState,
{ [evt.target.value]: evt.target.checked }
]);
}
};
return (
<div className="input-bar">
<form>
<input
type={type}
name={itemId}
value={itemId}
onChange={captureInput}
/>
<input name={itemId} type="submit" value="Add to Cart" />
</form>
</div>
);
};
export default InputComponent;
function ResultsRenderer() {
const [items, setItems] = useState([]);
useEffect(() => {
console.log(items);
}, [items]);
const renderListings = () => {
let listings = [
{ itemId: 1, title: "Hello" },
{ itemId: 2, title: "World" }
];
return listings
? listings.map((item) => {
return (
<div className="cart-row">
<InputComponent
type="checkbox"
className="cart-checkbox"
itemId={item.itemId}
setItems={setItems}
/>
<div key={item.itemId} className={item.itemId}>
<div>
Title<em>{item.Title}</em>
</div>
</div>
</div>
);
})
: null;
};
return <>{renderListings()}</>;
}
and here is the working demo: https://codesandbox.io/s/boring-cookies-t0g4e?file=/src/InputComponent.jsx

using ...prev in setState prohibits userEvent.type to type into input when react testing

I want to test my Log In Component, which consists of two input fields, whose values are determined by a single react state that is an object with two parameters. However, when I try the test only the first letter appears in the value of the selected input and not the rest of the word. I determined my use of ...prev when updating the state to be the issue. If I only use a single input field with one state it works fine!
Here is my component:
import {useState} from 'react';
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '' });
const handleChange = ({target}) => {
setLoginCredentials({[target.name]: target.value});
}
return (
<div className="login-container">
<h1>Log In</h1>
<div className="login-label-input">
<label htmlFor="name">Account Name
<input
type="name"
id="name"
name="name"
onChange={handleChange}
value={loginCredentials.name}
/>
</label>
<label htmlFor="name">Password
<input
type="password"
id="password"
name="password"
onChange={handleChange}
value={loginCredentials.password}
/>
</label>
</div>
State name: {loginCredentials.name}. State password: {loginCredentials.password}.
</div>
)
}
This works but if I include the password state:
export function Login () {
//Login Credentials
const [loginCredentials, setLoginCredentials] = useState({ name: '', password: '' });
const handleChange = ({target}) => {
setLoginCredentials((prev) => ({...prev, [target.name]: target.value}));
}
...
it does not pass the test. I does not throw an error but simply only adds the first letter of the string I am testing with:
test("log in with empty name input returns error message", async () => {
render(
<Login />
);
const nameField = screen.getByLabelText(/account name/i);
userEvent.type(nameField, 'test');
await waitFor(() => expect(nameField).toHaveValue('test'));
});
with the error:
expect(element).toHaveValue(test)
Expected the element to have value:
test
Received:
t
Is using ...prev bad or is this is a bug or what is going on?
It seems like you have to assign the new value to a different variable, I am not sure why this is necessary for the test but not in the app itself.
const handleChange = ({target}) => {
const newValue = target.value
setLoginCredentials((prev) => ({ ...prev, [target.name]: newValue }));
}

Using props.change to change the value of an input field in redux-form

I have a form that needs to retrieve the value of a field from my backend. It seems to retrieve the value successfully, but it does not update the value in the form.
How I use props.change to try to update field "foo":
const asyncValidate = (values, dispatch, props) => {
return axios
.get(URL)
.then( (response) => {
var whyNoChange = props.change("foo", response.data.foo)
console.log(whyNoChange)
});
}
Foo component:
const fooField = (props) => (
<Form.Field
error={props.meta.touched && props.meta.error ? true : false}>
<Input
placeholder='Change me'
onBlur={props.input.onBlur}
onFocus={props.input.onFocus}
onChange={(param, data) => props.input.onChange(data.value)}
value={props.input.value}
/>
{
props.meta.touched &&
props.meta.error &&
<span style={{color: '#9f3a38'}}>
{props.meta.error}
</span>
}
</Form.Field>
)
My form:
class MyForm extends Component {
// ...
render () {
return (
<Form>
<Field
name='foo'
component={fooField}
/>
</Form>
)
}}
MyForm = reduxForm({
form: 'myform',
validate,
asyncValidate
})(MyForm)
And finally what the console logs when it dispatches redux-form/change ...
{…}
>meta: Object { form: "myform", field: "foo", touch: false, … }
payload: "foo-from-backend"
type: "##redux-form/CHANGE"
But my foo-field value doesn't get updated. I checked the react developer tools and the value is just "". I followed the change directions as per the documentation. Any advice on where to go from here would be greatly appreciated. I am new to react and redux-form.
First of all, you have to dispatch it. Secondly, you are assigning a void function to a variable without calling it. Lastly, I believe that change accepts three arguments - form name, field name and value.
const asyncValidate = (values, dispatch, props) => {
return axios
.get(URL)
.then( (response) => {
dispatch(change("form name", "foo", response.data.foo))
});
}

React - object assign multiple dynamic input values

I am trying to set state of dynamically generated inputs. The initial tasks object where I want to set the new state looks like so:
The render method:
render(){
return(
<div>
<main className="content">
<form onSubmit={this.onSubmit}>
<div>
{Object.keys(this.state.dataGoal).map( (key, index) => {
return <div key={key}>
<label>{this.state.dataGoal[key]}</label>
<div className="input-wrap">
<input
type="text"
name={`${key}-task-${index}`}
value={this.state.tasks[key]}
onChange={this.handleInputChange} />
</div>
</div>;
})}
</div>
<div className="input-wrap">
<input
className="primary-btn"
type="submit"
value="Set my goal!"
onClick={this.formReset} />
</div>
</form>
</main>
</div>
);
}
and finally the handleInputChanged function:
handleInputChange = (e) => {
const value = e.target.value;
const name = e.target.name;
this.setState({
tasks: Object.assign({}, this.state.tasks, {[name]: value})
});
}
I want to set the new state of object when one of the inputs is changed. The desired result is to get the input value and set it to name key as an value in tasks object.
I also want to ask if the input names must be unique.
Thanks for any help,
Jakub
This looks like you're on the right path, the only thing missing is we will have to tell handleInputChange what index we want to update in tasks. We will have to use Object.assign twice because it's a nested structure. In this case if we assume the indices of dataGoal match up with tasks, we can pass in the index provided by the map function.
In our render function:
<input
type="text"
name={`${key}-task-${index}`}
value={this.state.tasks[key]}
onChange={(e) => this.handleInputChange(e, index)} />
// Notice: This will cause a performance hit
// We are binding 'this' using fat arrow every render but it shows the idea
Handling the input:
handleInputChange = (e, index) => {
const value = e.target.value;
const name = e.target.name;
const tasks = this.state.tasks;
const updatedTask = Object.assign({}, tasks[index], { [name]: value });
this.setState({
tasks: Object.assign({}, tasks, { [index]: updatedTask })
});
}
Input names don't have to be unique in this case, as the index will provide 'uniqueness'.
Using destructing instead of Object.assign:
handleInputChange = (e, index) => {
const value = e.target.value;
const name = e.target.name;
this.setState({
tasks: {
...this.state.tasks,
[index]: {
...this.state.tasks[index],
[name]: value
}
}
});
}

Categories

Resources