I'm updating one of my state properties without using the setter and using a normal variable assignment in js, that causes me the problem that the property state only updates once, so when i want to check a checkbox more than once the state in app does not update correctly, i think that the problem causing it is that i'm messing up that property assignment.
I searched for how to update a property of a state without overwritting the whole object and every post i find answering that question is using this.setState instead of declaring a state and using the declared setter method and i don't know how to adapt that this update to my code.
For example, the top post called "How to update nested state properties in React" declares his state like this:
this.state = {
someProperty: {
flag:true
}
}
While i'm declaring my state like this:
const [EditedTask, setEditedTask] = useState({
name: props.task.name,
completed: props.task.completed,
_id: props.task._id,
});
const { name, completed } = EditedTask;
And the top answer to that question is:
this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))
In this solution, with which object should i replace the this in the this.setState? Or the prevState param?
In my code i need to update the checkbox/ completed state, with this solution i'm only receiving the object EditedTask in my app component when i edit the checkbox once, if i check it more than once the state doesn't update in app, meanwhile if i edit the name the state updates in app correctly with both the name and completed propertys.
import React, { useState } from "react";
import "../App.css";
const EditTask = (props) => {
const [EditedTask, setEditedTask] = useState({
name: props.task.name,
completed: props.task.completed,
_id: props.task._id,
});
const { name, completed } = EditedTask;
const onChange = (e) => {
setEditedTask({
...EditedTask,
[e.target.name]: e.target.value,
});
};
const onSubmit = (e) => {
e.preventDefault();
setEditedTask(EditedTask);
props.saveEditedTask(EditedTask);
props.changeEdit(false);
};
return (
<>
<form onSubmit={onSubmit}>
<div className="form-group">
<label>Task Id: ${props.task._id}</label>
<div className="inputEdit">
<input
type="text"
placeholder={props.msg}
name="name"
onChange={onChange}
value={name}
/>
</div>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
defaultChecked={completed}
value={completed}
onChange={() => (EditedTask.completed = !EditedTask.completed)}
name="completed"
/>
<label className="form-check-label">Completed</label>
</div>
<div className="submit">
<button type="submit">Edit</button>
</div>
</form>
</>
);
};
export default EditTask;
I tried replacing the onChange={() => (EditedTask.completed = !EditedTask.completed)} like the top answer/ my onChange for the name value with something like this but it doesn't update the completed value
const updateCompleted = (e) => {
setEditedTask({
...EditedTask,
[e.target.name]: !e.target.value,
});
};
You can try something like this in your setter functions.
const updateCompleted = (e) => {
setEditedTask((currentState) => ({
...currentState,
[e.target.name]: !e.target.value
})
)};
This solution will give you your previous state, which your can use to update your state.
Related
I am building a form where the hotel owners will add a hotel and select a few amenities of the same hotel. The problem is If I use state in the onChange function the checkbox tick is not displayed. I don't know where I made a mistake?
import React from "react";
import { nanoid } from "nanoid";
const ListAmenities = ({
amenities,
className,
setHotelAmenities,
hotelAmenities,
}) => {
const handleChange = (e) => {
const inputValue = e.target.dataset.amenitieName;
if (hotelAmenities.includes(inputValue) === true) {
const updatedAmenities = hotelAmenities.filter(
(amenitie) => amenitie !== inputValue
);
setHotelAmenities(updatedAmenities);
} else {
//If I remove this second setState then everything works perfectly.
setHotelAmenities((prevAmenities) => {
return [...prevAmenities, inputValue];
});
}
};
return amenities.map((item) => {
return (
<div className={className} key={nanoid()}>
<input
onChange={handleChange}
className="mr-2"
type="checkbox"
name={item}
id={item}
data-amenitie-name={item}
/>
<label htmlFor={item}>{item}</label>
</div>
);
});
};
export default ListAmenities;
The problem is that you are using key={nanoid()}. Instead, using key={item] should solve your probem.
I believe your application that uses ListAmenities is something like this:
const App = () => {
const [hotelAmenities, setHotelAmenities] = useState([]);
return (
<ListAmenities
amenities={["A", "B", "C"]}
className="test"
setHotelAmenities={setHotelAmenities}
hotelAmenities={hotelAmenities}
/>
);
};
In your current implementation, when handleChange calls setHotelAmenities it changed hotelAmenities which is a prop of ListAmenities and causes the ListAmenities to rerender. Since you use key={nanoid()} react assumes that a new item has been added and the old one has been removed. So it re-renders the checkbox. Since there is no default value of checkbox, it is assumed that it is in unchecked state when it is re-rendered.
I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here
I have a code like this
const ChildComponent = ({ products, setProducts }) => (
<form>
<input type="text" value={products.name} onChange={(e) => setProducts(e.target.value)} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState(
{
id: 1,
name: "Test",
}
);
useEffect(() => {
// Where i call API to get list product and set it to child component
}, [])
return <ChildComponent products={products} setProducts={setProducts} />
}
for some reason , i can ONLY update state of ParentComponent in ChildComponent. It's work but i think it's so weird, that look like i change props of child component everytime when i make a edit of input. Can any one tell me that is an anti pattern or not.Sorry about my bad English. Thank you so much!
It's not an anti-pattern to pass the state object and state updater function as props, but this offloads the responsibility to update state correctly and maintain the state invariant to consuming components.
As you can see, your child component already messes up and changes the state shape/invariant from Object to String.
const [products, setProducts] = useState({ // <-- object
id: 1,
name: "Test",
});
... child
onChange={(e) => setProducts(e.target.value)} // <-- string value
On the subsequent render attempting to access value={products.name} in the child will fail as now products is a string.
I typically suggest declaring a handler function to do the state update and pass that instead.
In your snippet it seems the child component is more a "controlled input" meaning it's an input tag with a value and onChange handler. This is an example refactor I would do.
const ChildComponent = ({ value, onChange, onSubmit }) => (
<form onSubmit={onSubmit}>
<input type="text" value={value} onChange={onChange} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState({
id: 1,
name: "Test",
});
const changeHandler = e => {
setProducts(products => ({
...products,
name: e.target.value,
}));
};
const onSubmit = e => {
e.preventDefault();
// handle the form submission
};
useEffect(() => {
// Where i call API to get list product and set it to child component
}, []);
return (
<ChildComponent
value={products}
onChange={changeHandler}
onSubmit={submitHandler}
/>
);
}
This way the parent maintains control over both the state updates and how the form data is submitted. The child hasn't any idea what the value represents and it isn't trying to update anything in any way, but simply passing back out the events.
I have just started learning react.
I have a component which is calling the weather API and fetching data for user provided location, user input is getting updated under userLoc but somehow the state is not getting updated for finalLoc and whenever I console log it, it is showing undefined.
const Inputs = props => {
const [userLocation, setUserLocation] = React.useState('')
const [finalLocation, setFinalLocation] = React.useState('')
function fetchLocation(e) {
setUserLocation (e.target.value)
}
function fetchDetails(e) {
e.preventDefault()
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
const API_KEY = '&API_KEY'
let total = baseURL + userLocation + API_KEY
console.log(userLocation) // Outputs the input value
setFinalLocation(userLocation)
console.log(finalLocation) // Comes as blank
console.log(total);
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={loc.userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={loc.finalLoc} results={loc.result} />
</div>
)
}
const Inputs = props => {
const [finalLoc, setFinalLoc] = React.useState("");
const [userLoc, setUserLoc] = React.useState("");
const [data, setData] = React.useState([]);
function fetchLocation(e) {
userLoc( e.target.value )
}
function fetchDetails(e) {
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
let API_KEY = '&appid=API_KEY'
let total = baseURL + loc.userLoc + API_KEY
setFinalLoc( userLoc )
fetch(total)
.then(response => response.json())
.then(data => {
setData( data )
})
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={ userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={ finalLoc} results={data} />
</div>
)
}
If you update a state when using useState you don't write it as
setState({state: //data})
But
setState(//data)
The updating method in useState is similar to class component setState method but you have to update it differently.
See more about useState here.
If your state is an object you should update it as an object:
const [state, setState] = useState({
name: "John",
lastName: "Due",
});
setState({
name: "Liam",
lastName: "call",
});
BTW,
If you are updating multiple values in your state, You should update it in one function.#1
Note:
Each time you're updating your state cuase React to re-render and slow your app.
For better performance you should always try to re-render your app as less as possible.
Edit:
#1 if your state is an object you should update it in one function.
You can use the functional form. The function will receive the previous value, and return an updated value. so to update the state we have to make copy of previous value with updated one and then set the state with updated values
setLoc((loc) => ({ ...loc, finalLoc: loc.userLoc }));
same when you get result data keep copy of previous state
setLoc((loc) => ({ ...loc, result: data }));
Introduction
Yesterday I followed an advanced tutorial from Kent C. Dodds where he explained how to connect an input to localstorage which then handles the setting of value, change of values etc and automatically sync with LocalStorage in react.
At the first place this works pretty well for normal components. However, for example the custom checkboxes which I have in my app do not work with the logics. I tried to alter the logics a bit but it seems that I didn't got far with it.
The Problem
Currently my custom checkbox component does not connect / work with the hoc LocalStorageFormControl.
Project info
I have made a CodeSandbox for you to play around with: https://codesandbox.io/s/eager-curie-8sj1x
The project is using standard bootstrap with scss stylings. The CustomCheckbox consists of two elements: the main div and the actual input itself. Currently the matching value in state will trigger className change in one of the elements to allow custom styling.
For any further questions please comment below. Thanks in advance for all the help.
Resources
Kent C. Dodds - Tutorial resource
CodeSandBox Project
The problems were:
The LocalStorageFormControl component didn't update the state when
it gets the initial value from localStorage.
The input didn't update the state onChange as it didn't have onChange
handler.
The CustomCheckboxGroup component didn't have a name prop which is used
as a part of the key in the localStorage
The solution is as following:
App.js
import React, { useEffect, useState } from "react";
// Bootstrap
import { Row, Col, Form } from "react-bootstrap";
import CustomCheckboxGroup from "./CustomCheckboxGroup";
// Function that calls all functions in order to allow the user to provide their own onChange, value etc
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));
// Connect any <input /> to LocalStorage and let it manage value / onChange
function LocalStorageFormControl({
children,
formControl = React.Children.only(children),
lsKey = `lsfc:${formControl.props.name}`,
updateInitialState
}) {
const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState(() => {
return (
window.localStorage.getItem(lsKey) || formControl.props.defaultValue || ""
);
});
// Let the user control the value if needed
if (
formControl.props.value !== undefined &&
formControl.props.value !== value
) {
setValue(formControl.props.value);
}
useEffect(() => {
if (hasChanged) {
if (value) {
window.localStorage.setItem(lsKey, value);
} else {
window.localStorage.removeItem(lsKey);
}
} else {
if (value) {
// if hasChanged is false and there is value that means there was a value in localStorage
setHasChanged(true);
// update the state
updateInitialState(value);
}
}
}, [value, lsKey, hasChanged, updateInitialState]);
return React.cloneElement(React.Children.only(children), {
onChange: callAll(formControl.props.onChange, e => {
setHasChanged(true);
setValue(e.target.value);
}),
value,
defaultValue: undefined
});
}
const checkboxes = [
{
label: "Dhr",
name: "aanhef-dhr",
stateName: "salutation",
value: "De heer"
},
{
label: "Mevr",
name: "aanhef-mevr",
stateName: "salutation",
value: "Mevrouw"
}
];
export default function App() {
const [state, setState] = useState({});
function handleSubmit(e) {
e.preventDefault();
console.log("Handling submission of the form");
}
function onChange(e, stateName) {
e.persist();
setState(prevState => ({ ...prevState, [stateName]: e.target.value }));
}
// Log the state to the console
console.log(state);
return (
<Row>
<Col xs={12}>
<Form
id="appointment-form"
onSubmit={handleSubmit}
noValidate
style={{ marginBottom: 75 }}
>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, "test-textfield": value });
}}
>
{/* Add onChange handler to update the state with input value*/}
<input
type="text"
name="test-textfield"
onChange={e => {
setState({ ...state, "test-textfield": e.target.value });
}}
/>
</LocalStorageFormControl>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, salutation: value });
}}
>
<CustomCheckboxGroup
checkboxes={checkboxes}
key="salutation"
label="Salutation"
name="salutation"
onChange={(e, stateName) => onChange(e, stateName)}
required={true}
value={state.salutation}
/>
</LocalStorageFormControl>
</Form>
</Col>
</Row>
);
}
CustomCheckboxGroup.js
import React from "react";
// Bootstrap
import { Form, Row, Col } from "react-bootstrap";
export default ({ onChange, value, name, label, className, checkboxes }) => (
<Row>
<Col xs={12}>
<Form.Label>{label}</Form.Label>
</Col>
<Col>
<Form.Group className="d-flex flex-direction-column">
{checkboxes.map((checkbox, key) => {
return (
<div
key={key}
className={
checkbox.value === value
? "appointment_checkbox active mr-2 custom-control custom-checkbox"
: "appointment_checkbox mr-2 custom-control custom-checkbox"
}
>
<input
name={name}
type="checkbox"
value={checkbox.value}
onChange={e => onChange(e, checkbox.stateName)}
checked={value === checkbox.value}
id={"checkbox-" + checkbox.name}
className="custom-control-input"
/>
<label
className="custom-control-label"
htmlFor={"checkbox-" + checkbox.name}
>
{checkbox.label}
</label>
</div>
);
})}
</Form.Group>
</Col>
</Row>
);
I have some advice about your code:
Use radio buttons instead of checkboxes if you allow the user to choose one option only. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio
You can persist the whole state object if you would like to by replacing this:
const [state, setState] = useState({});
with this:
// Get the saved state in local storage if it exists or use an empty object
// You must use JSON.parse to convert the string back to a javascript object
const initialState = localStorage.getItem("form-state")
? JSON.parse(localStorage.getItem("form-state"))
: {};
// Initialize the state with initialState
const [state, setState] = useState(initialState);
// Whenever the state changes save it to local storage
// Notice that local storage accepts only strings so you have to use JSON.stringify
useEffect(() => {
localStorage.setItem("form-state", JSON.stringify(state));
}, [state]);