I can't update state when submitted in a form - reactjs

I need to update the state on main page, the problem is that I update values 3 levels down...
This component is where I get all the cities, putting the data on the State and map through cities.
CitiesPage.tsx
export const CitiesPage = () => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
getCities().then(setCities);
}, []);
return (
<>
<PageTitle title="Cities" />
<StyledCitySection>
<div className="headings">
<p>Name</p>
<p>Iso Code</p>
<p>Country</p>
<span style={{ width: "50px" }}></span>
</div>
<div className="cities">
{cities.map((city) => {
return <CityCard key={city.id} city={city} />;
})}
</div>
</StyledCitySection>
</>
);
};
On the next component, I show cities and options to show modals for delete and update.
CityCard.tsx.
export const CityCard = ({ city }: CityProps) => {
const [showModal, setShowModal] = useState(false);
const handleModal = () => {
setShowModal(true);
};
const handleClose = () => {
setShowModal(false);
};
return (
<>
{showModal && (
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} />
</ModalPortal>
)}
<StyledCityCard>
<p className="name">{city.name}</p>
<p className="isoCode">{city.isoCode}</p>
<p className="country">{city.country?.name}</p>
<div className="options">
<span className="edit">
<FiEdit size={18} onClick={handleModal} />
</span>
<span className="delete">
<AiOutlineDelete size={20} />
</span>
</div>
</StyledCityCard>
</>
);
};
and finally, third levels down, I have this component.
EditCityForm.tsx.
export const EditCityForm = ({ city, closeModal }: Props) => {
const [updateCity, setUpdateCity] = useState<UpdateCity>({
countryId: "",
isoCode: "",
name: "",
});
const handleChange = (
evt: ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { target } = evt;
setUpdateCity({ ...updateCity, [target.name]: target.value });
};
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
updateCityHelper(cityId, updateCity);
closeModal(false);
};
useEffect(() => {
setUpdateCity({
isoCode: city.isoCode,
name: city.name,
countryId: city.country?.id,
});
}, [city]);
return (
<form onSubmit={handleSubmit}>
<Input
label="Iso Code"
type="text"
placeholder="Type a IsoCode..."
onChange={handleChange}
name="isoCode"
value={updateCity.isoCode}
/>
<Input
label="Name"
type="text"
placeholder="Paste a Name.."
onChange={handleChange}
name="name"
value={updateCity.name}
/>
<CountrySelect
label="Country"
onChange={handleChange}
value={city.country?.name || ""}
name="countryId"
/>
<Button type="submit" color="green" text="Update" />
</form>
);
};
Edit form where retrieve data passed from CityCard.tsx and update State, passing data to a function that update Info, closed modal and... this is where I don't know what to do.
How can I show the info updated on CitiesPage.tsx when I submitted on EditCityForm.tsx
Any help will be appreciated.
Thanks!

You are storing the updated value in a different state, namely the updateCity state, but what you should be doing is update the origional cities state. While these two states are not related, and at the same time your UI is depend on cities state's data, so if you wish to update UI, what you need to do is update cities' state by using it's setter function setCities.
Just like passing down state, you pass it's setters as well, and use the setter function to update state's value:
// CitiesPage
{cities.map((city) => {
return <CityCard key={city.id} city={city} setCities={setCities} />;
})}
// CityCard
export const CityCard = ({ city, setCities }: CityProps) => {
// ...
return (
// ...
<ModalPortal onClose={handleClose}>
<EditCityForm city={city} closeModal={setShowModal} setCities={setCities} />
</ModalPortal>
// ...
)
}
// EditCityForm
export const EditCityForm = ({ city, closeModal, setCities }: Props) => {
// const [updateCity, setUpdateCity] = useState<UpdateCity>({ // no need for this
// countryId: "",
// isoCode: "",
// name: "",
// });
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { id: cityId } = city;
setCities(); // your update logic
closeModal(false);
};
}

I'd advise you to use React-Redux or Context API whenever you have nested structures and want to access data throughout your app.
However in this case you can pass setCities and cities as a prop to CityCard and then pass this same prop in the EditCityForm component and you can do something like this in your handleSubmit.
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
let updatedCities = [...cities];
updatedCities.forEach(el => {
if(el.id == updateCity.id) {
el.countryCode = updateCity.countryCode;
el.name = updateCity.name;
el.isoCode = updateCity.isoCode;
}
})
setCities(updatedCities);
closeModal(false);
};

Related

Problem Chakra Slider in React Function: undefined event.target

I'm changing React Class into function for a component with text and number input from database (mongodb). The function handleInputChange works fine for input text and number, but not with the slider. Don't know, how to solve. TypeError: Cannot destructure property 'name' of 'event.target' as it is undefined.
const ProgrammUpdate = props => {
const {id} = useParams();
const initialProgrammState = {
id: null,
title: "",
description: "",
volume_left: "",
volume_right: "",
}
const [currentProgramm, setCurrentProgramm] = useState(initialProgrammState);
const {user} = ChatState();
const toast = useToast();
const history = useHistory();
const [message, setMessage] = useState("");
const bg = useColorModeValue('brand.200', 'brand.100')
const color = useColorModeValue('gray.800', 'white')
const getProgramm = programmId => {
HoerprogrammDataService.get(programmId)
.then(response => {
setCurrentProgramm(response.data);
console.log(response.data);
})
.catch(e => {
console.log(e);
})
}
useEffect(() => {
if (id)
getProgramm(id);
// eslint-disable-next-line
console.log(id)
}, [id]);
const handleInputChange = event => {
const {name, value} = event.target;
setCurrentProgramm({...currentProgramm, [name]: value});
};
const updateProgramm = () => {
HoerprogrammDataService.update(id, currentProgramm)
.then(response => {
console.log(response.data);
// setMessage("The tutorial was updated successfully!");
history.push('/programme');
})
.catch(e => {
console.log(e);
});
};
const deleteProgramm = () => {
HoerprogrammDataService.delete(currentProgramm.id)
.then(response => {
console.log(response.data);
history.push('/programme');
})
.catch(e => {
console.log(e);
});
};
return<LandingLayout>
<h2>Test</h2>
{currentProgramm ? (<>
<Input
placeholder={currentProgramm.title} size='md'
type="text"
className="form-control"
id="title"
required
value={currentProgramm.title}
onChange={handleInputChange}
name="title"
/>
<Input
placeholder={currentProgramm.volume_left}
type="text"
className="form-control"
id="volume_left"
value={currentProgramm.volume_left}
onChange={handleInputChange}
name="volume_left"
/>
<Slider
flex='1'
value={currentProgramm.volume_left}
onChange={handleInputChange}
orientation='horizontal'
minH='20'
name="volume_left"
min={0} max={10} step={1}
>
<SliderMark value={0} mt='12' ml='-1.5' fontSize='md'>0</SliderMark>
<SliderMark value={5} mt='12' ml='-1.5' fontSize='md'>5</SliderMark>
<SliderMark value={10} mt='12' ml='-1.5' fontSize='md'>10</SliderMark>
<SliderTrack bg='blue.200'>
<SliderFilledTrack bg="blue.400" />
</SliderTrack>
<SliderThumb bg="blue.400" fontSize='md' boxSize='40px' children={currentProgramm.volume_left}>{currentProgramm.volume_left}</SliderThumb>
</Slider>
<Routelink to={"/programme"} className={"nav-link"}>
<Button variant={ "outline"} colorScheme={"red"}>Abbruch</Button>
</Routelink>
<button className="badge badge-danger mr-2" onClick={deleteProgramm}>
Delete
</button>
<button
type="submit"
className="badge badge-success"
onClick={updateProgramm}
>
Update
</button>
<p>{message}</p>
</>
) : (
<Box d="flex" alignItems="center" justifyContent="center" h="100%">
<Text fontSize="3xl" pb={3} fontFamily="Work sans">Programm nicht geladen</Text>
</Box>
)}
</LandingLayout>
}
export default ProgrammUpdate;
You were getting this error because Slider onChange method passes the actual value, not the event object. On the other hand, the Input onChange method passes the event.
Your Slider onChange method will look something like this-
const handleSliderChange = (val) => {
setCurrentProgramm({...currentProgramm, volume_left: val})
}

React.js working code breaks on re-render

I have a weird bug, where my code works on first attempt, but breaks on page re-render.
I've created a filter function using an object with filter names and array of filter values:
const filterOptions = {
'size': ['s', 'm', 'l'],
'color': ['black', 'white', 'pink', 'beige'],
'fit': ['relaxed fit','slim fit', 'skinny fit', 'oversize'],
'pattern': ['patterned', 'spotted', 'solid color'],
'material': ['wool', 'cotton', 'leather', 'denim', 'satin']
}
The idea was to create a separate object with all the values and corresponding 'checked' attribute and than use it to check if checkbox is checked:
const [checkedValue, setCheckedValue] = useState({})
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))}, [])
FilterValue here is array of values from FilterOptions:
<div className='popper'>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
There is onChange function as wel, which could be a part of problem:
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}}
I've tried keeping filterOptions in parent component and in Context, but it gives exactly the same result. It always work as planned on first render, and on next render it shows this error, until you delete the checked attribute of input. I've noticed that on re-render the 'checkedValue' object returns as empty, but I can't find out why. Would be really helpful if somebody could explain me a reason.
Uncaught TypeError: Cannot read properties of undefined (reading 'checked')
Edit: full code looks like this:
Parent Component
const Filter = () => {
return (
<div className='filter'>
<div className="price-filter">
<p>Price: </p>
<Slider onChange={handleSliderChange} value={[min, max]} valueLabelDisplay="on" disableSwap style={{width:"70%"}} min={0} max={250} />
</div>
<Divider />
<ul className='filter-list'>
{Object.entries(filterOptions).map((filter, i) => {
return (
<Fragment key={`${filter[0]}${i}`}>
<FilterOption className='filter-option' filterName={filter[0]} filterValue={filter[1]} />
<Divider key={`${i}${Math.random()}`} />
</Fragment>
)
})}
</ul>
</div>
)
}
Child Component
const FilterOption = ({ filterName, filterValue }) => {
const { checkedValue, setCheckedValue, activeFilters, setActiveFilters, filterOptions } = useContext(FilterContext)
useEffect(() => {
const filterValuesArray = Object.values(filterOptions).flat()
filterValuesArray.map(filter => setCheckedValue(currentState => ({...currentState, [filter]: { ...currentState[filter], checked: false }})))
}, [])
const handleCheckbox = (event) => {
const value = event.target.value;
setCheckedValue({...checkedValue, [value]: { ...checkedValue[value], checked: !checkedValue[value].checked }})
if(activeFilters.includes(value)) {
const deleteFromArray = activeFilters.filter(item => item !== value)
setActiveFilters(deleteFromArray)
} else {
setActiveFilters([...activeFilters, value])
}
}
return (
<div className='popper' key={filterName}>
{filterValue.map(value => {
return (
<div key={`${value}`} className='popper-item'>
<label className='popper-label'>{value}</label>
<input onChange={handleCheckbox} checked={checkedValue[value].checked} type='checkbox' value={value} className="popper-checkbox" />
</div>
)}
)}
</div>
)

All other input data get clear when I i hit handleSubmit

Hi i am working on a React application where there are four options.when a user select an option corresponding input element will be added to the wrapper.In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can acheive removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders.
But when I submit the input it will appear my data perfectly and when i restart the page and just click into edit and hit submit with the defaultValue it just clear all the data and send back to my backend with undefined value like this: [ undefined, undefined, undefined, undefined ]
Here is my full component:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(teamData.rules);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const list = [...inputs];
list.splice(index, 1);
setInputs(list);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
console.log("teamData.rules", teamData.rules);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
<p className="team-agreement-rules-description">{description}</p>
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={`${data}-${idx}`}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
How can I fix this error?
My thought is the problem is around [inputs, setInputs]
Try this
<input
//..
onChange={(event) => handleChange(event.target.value)}
//..
/>
then in your "handleChange" function
const handleChange = (event) => {
const { name, value } = event;
//....
};

map function to work with event.target.name, have multiple values tagged to index of map

The below functionality is only capable of running only one component (i.e. "ComponentTwo"), I want to modify it to have more component, but issue is as i am using map function to loop through component to map "value", same value will be passed to all the component.
In the code there is two function for handling the change currently i am using the "handleInputChange" which take value as input but i want it to work with name so that i can have name to distinguish between components, below is one commented function which i am trying to implement, but is not working.
p.s. if you need any clarifications let me know in comment section.
link to code:https://codesandbox.io/s/happy-hugle-mfstd?file=/src/App.js
import React, { Component, useState } from "react";
export default function App() {
const [inputValues, setInputValues] = useState(["Test"]);
const addNewInputvalue = () => {
setInputValues((prev) => {
return [...prev, ""];
});
};
const removeInput = (index) => {
setInputValues((prev) => {
const copy = [...prev];
copy.splice(index, 1);
return copy;
});
};
// const handleChange = (event) => {
// event.persist()
// setData(prev => ({ ...prev, [event.target.name]: event.target.value }))
// }
const handleInputChange = (index, value) => {
setInputValues((prev) => {
const copy = [...prev];
copy[index] = value;
return copy;
});
};
const consoleAllValues = () => {
console.log(inputValues);
};
return (
<div className="App">
<button onClick={addNewInputvalue}>New Input</button>
{inputValues.map((val, i) => {
return (<div>
<ComponentTwo
key={i}
index={i}
value={val}
onChange={handleInputChange}
removeInput={() => removeInput(i)}
/>
<ComponentThree />
<ComponenFour />
</div>
>
);
})}
<button onClick={consoleAllValues}>console log all values</button>
</div>
);
}
const ComponentTwo = (props) => {
return (
<div>
<p>Input: {props.index}</p>
<input
name={"right_value"}
onChange={(e) => props.onChange(props.index, e.target.value)}
type="text"
value={props.value}
/>
<button onClick={props.removeInput}>Remove Input</button>
</div>
);
};
Instead of using an array to store your values, you should create a key value object. See the changes I've made regarding your state, the way you iterate through the object inside your return statement and all the functions that manipulate your state.
import React, { Component, useState } from "react";
export default function App() {
const [inputValues, setInputValues] = useState({
'componentTwo': 'val1',
'componentThree': 'val2',
'componentFour': 'val3',
});
const addNewInputvalue = (name, value) => {
setInputValues((prev) => {
return {
...prev,
[name]: value,
}
});
};
const removeInput = (name) => {
setInputValues((prev) => {
const copy = {...prev};
delete copy[name];
return copy;
});
};
const handleInputChange = (name, value) => {
setInputValues((prev) => {
return {
...prev,
[name]: value,
};
});
};
const consoleAllValues = () => {
console.log(inputValues);
};
return (
<div className="App">
<button onClick={addNewInputvalue}>New Input</button>
{Object.keys(inputValues).map((name, i) => {
return (<div>
<ComponentTwo
key={name}
index={i}
value={inputValues[name]}
onChange={(value) => handleInputChange(name, value)}
removeInput={() => removeInput(name)}
/>
<ComponentThree />
<ComponenFour />
</div>
>
);
})}
<button onClick={consoleAllValues}>console log all values</button>
</div>
);
}
const ComponentTwo = (props) => {
return (
<div>
<p>Input: {props.index}</p>
<input
name={"right_value"}
onChange={(e) => props.onChange(e.target.value)}
type="text"
value={props.value}
/>
<button onClick={props.removeInput}>Remove Input</button>
</div>
);
};

multiple layer of forwardRef and useImperativeHandle

I need to use ref to get a grandchild component's state, and I customized it by using useImperativeHandle, the whole code is simplified
here.
function App() {
const ref = useRef(null);
return (
<div className="App">
<button
onClick={() => console.log(ref.current.name, ref.current.age)}
>click me</button>
<FilterFather ref={ref} />
</div>
);
}
const FilterFather = (_, ref) => {
const filter1Ref = useRef(null);
const filter2Ref = useRef(null);
useImperativeHandle(ref, () => ({
name: filter1Ref.current.name,
age: filter2Ref.current.age,
}))
return (
<>
<Filter1 ref={filter1Ref}/>
<Filter2 ref={filter2Ref} />
</>
)
}
export default forwardRef(FilterFather);
const Filter1 = (props, ref) => {
const [name, setName] = useState('lewis')
useImperativeHandle(ref, () => ({
name
}), [name])
return (
<>
<div>
name:
<input
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
</>
)
}
const Filter2 = (props, ref) => {
const [age, setAge] = useState(18)
useImperativeHandle(ref, () => ({
age
}), [age])
return (
<>
<div>
age:
<input
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
</>
)
}
export default {
Filter1: forwardRef(Filter1),
Filter2: forwardRef(Filter2),
}
one layer of forwardRef and useImperativeHandle works fine, two layers went wrong
Your imperative handle in FilterFather is not required. It doesn't add/remove anything to the handle. You can just forward it directly:
const FilterFather = (_, ref) => {
return <Filter ref={ref} />;
};
Also there is a problem with it because it will not update correctly.
useImperativeHandle(ref, () => ({
name: filterRef.current.name,
age: filterRef.current.age,
}), [filterRef]) // this will not update correctly
You passed filterRef as a dependency but filterRef is static and will not change even when filterRef.current.name or filterRef.current.age changes.
Note that useImperativeHandle is not supposed to be used to read state from child components. It is actually discouraged to use any kind of imperative methods in react except there is no other way. This is most of the time the case if you work with 3rd party libraries that are imperative in nature.
EDIT:
Given your updated code the react way to do that is to lift state up. It doesn't require any refs:
export default function App() {
const [values, setValues] = useState({
name: "lewis",
age: 18
});
const handleChange = useCallback(
(key, value) => setValues(current => ({ ...current, [key]: value })),
[]
);
return (
<div className="App">
<button onClick={() => console.log(values.name, values.age)}>
click me
</button>
<FilterFather values={values} onChange={handleChange} />
</div>
);
}
const FilterFather = props => {
return (
<>
<Filter label="Name" name="name" {...props} />
<Filter label="Age" name="age" {...props} />
</>
);
};
const Filter = ({ label, name, values, onChange }) => {
const handleChange = useCallback(e => onChange(name, e.target.value), [
name,
onChange
]);
return (
<>
<div>
<label>{label}:</label>
<input value={values[name]} onChange={handleChange} />
</div>
</>
);
};

Resources