React.js working code breaks on re-render - reactjs

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

Related

Changes don't render in React

I have array of items and searching items function, that returns another array. When I delete or edit item finded items changes don't render, but when search string has another value React render changes.
I know that useEffect can resolve this problem, but dont what to put in callback.
How can resolve this problem?
export const ToDoList = (props: PropsType) => {
const [searchQuery, setSearchQuery] = useState('')
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
},
[searchQuery])
return (
{props.ToDoData.length ?
<>
<input
...
onChange={e => setSearchQuery(e.target.value)}
/>
<ItemsList
...
items={
searchQuery ?
searchedItems :
props.ToDoData
}
/>
</> :
...
}
)
}
export const ItemsList = (props: PropsType) => {
const [editedText, setEditedText] = useState('')
const onDeleteItem = (id: number) => {
props.dispatch(deleteItem(id))
},
onEditItemMode = (id: number, text: string) => {
props.dispatch(setEditMode(true, id))
setEditedText(text)
},
onEditText = (id: number) => {
props.dispatch(setEditedTextInItem(id, editedText))
props.dispatch(setEditMode(false, id))
setEditedText('')
},
onToggleCompletedStatus = (id: number, status: string) => {
...
}
return (
{props.items.length ?
props.items.map((object) => (
<div
className="Item"
key={object.id}
>
{props.inEditMode.some((id: number) => id === object.id) ?
<>
<input
value={editedText}
onChange={e => { setEditedText(e.currentTarget.value) }}
/>
<button onClick={() => onEditText(object.id)}>
Change text
</button>
</> :
<>
<div className="Item__textBlock">
<input
type='checkbox'
onClick={() => { onToggleCompletedStatus(object.id, object.status)}}
/>
<span className={
object.status === 'completed' ?
'completed' :
'in process'
}>
{object.text}
</span>
</div>
<div className="Item__buttonBlock">
<button
className="Item__button"
disabled={props.inEditMode.length !== 0}
onClick={() => onEditItemMode(object.id, object.text)}
>
<img src={editImg} />
</button>
<button
className="Item__button"
onClick={() => { onDeleteItem(object.id) }}
>
<img src={removeImg} />
</button>
</div>
</>
}
</div>
)) :
...
}
)
}
// This code creates a list that is ONLY updated when searchQuery is updated
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
}, [searchQuery]);
// This code creates the list every time the component renders,
// so it will always be correct
const searchedItems = props.ToDoData.filter(item => item.text.includes(searchQuery))
// If you absolutely need to optimize the render of this component
// This code will update the list whenever the reference for ToDoData is updated as well
const searchedItems = useMemo(() => {
return props.ToDoData.filter(item => item.text.includes(searchQuery))
}, [searchQuery, props.ToDoData]);

I can't update state when submitted in a form

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

ToDo rendering data object

I was just testing around building a dummy todo list and was trying to figure out something. While setting the new state with the new task object that includes an id and a text. Well everything works well just my issue when I console.log(allTasks) it starts only to show the array of data after I have added the second task ?
const SearchInput = () => {
const [taskValue, setTaskValue] = useState("");
const [allTasks, setAllTasks] = useState([]);
const handleChange = (e) => {
setTaskValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (taskValue !== "") {
setAllTasks([
...allTasks,
{ id: allTasks.length + 1, text: taskValue.trim() },
]);
}
setTaskValue("");
console.log(allTasks);
};
return (
<>
<Form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Add a task..."
value={taskValue}
onChange={handleChange}
/>
<button>Submit the Task</button>
</Form>
<div>
{allTasks.length <= 0 ? (
<p>No tasks</p>
) : (
<ul>
{allTasks.map((task) => (
<li key={task.id}> {task.text} </li>
))}
</ul>
)}
</div>
</>
);
};
Here you get updated value and there is a conditional change I hope you will like.
Thanks
const SearchInput = () => {
const [taskValue, setTaskValue] = React.useState("");
const [allTasks, setAllTasks] = React.useState([]);
const handleChange = (e) => {
setTaskValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (taskValue !== "") {
setAllTasks([
...allTasks,
{ id: allTasks.length + 1, text: taskValue.trim() },
]);
}
setTaskValue("");
};
console.log(allTasks);
return (
<>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Add a task..."
value={taskValue}
onChange={handleChange}
/>
<button>Submit the Task</button>
</form>
<div>
{!allTasks.length && <p>No tasks</p>}
{!!allTasks.length &&
<ul>
{allTasks.map((task) => (
<li key={task.id}> {task.text} </li>
))}
</ul>
}
</div>
</>
);
};
According to the docs, setState is async in nature, which means it will execute only after execution of all synchronous code. And setState takes a callback as the second parameter which you can use to log it as expected.
setAllTasks([
...allTasks,
{ id: allTasks.length + 1, text: taskValue.trim() },
], ()=>{
console.log(alltasks)
});
Reference

React validation of dynamic checkboxes

I have 7 checkboxes that are rendered within a map method.. each checkbox has a question and when all checkboxes are checked, a button should be activated.. can someone please tell me how can I validate this using useState() hook.. I know how to do this with one checkbox, but I don't know how to handle multiple checkboxes that are rendered within a map method. A code example should be very helpful. Any help will be appreciated.
const [isChecked, setIsChecked] = useState(false);
{ questions.map(q => (
<input
type={q.radio ? "radio" : "checkbox"}
onClick={() => setIsChecked(!isChecked)}
value={isChecked}
id={option.id}
value={option.value}
name={`${q.name}`}
/>
There are 7 checkboxes rendered with this map method. How should I handle the state in this case?
If your questions are dynamic list you can use below approach:
const QUESTIONS = ["Do you use Stackoverflow?", "Do you asked a question?"];
function Confirmation({ data }) {
const [questions, setQuestions] = React.useState(
data.map((question, index) => {
return { id: index, text: question, checked: false };
})
);
const handleClick = (id, checked) => {
const newQuestions = [...questions];
const index = newQuestions.findIndex((q) => q.id === id);
newQuestions[index].checked = checked;
setQuestions(newQuestions);
};
const isButtonDisabled = questions.some((q) => q.checked === false);
return (
<div>
{questions.map((question) => (
<React.Fragment>
<input
key={question.id}
name={`question-${question.id}`}
type="checkbox"
checked={question.checked}
onClick={(e) => handleClick(question.id, e.target.checked)}
/>
<label htmlFor={`question-${question.id}`}>{question.text}</label>
</React.Fragment>
))}
<button disabled={isButtonDisabled}>Confirm</button>
</div>
);
}
ReactDOM.render(<Confirmation data={QUESTIONS} />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
To add to thedude's answer, you can dynamically adjust the checked array based on changes to your questions array so that you don't have to manually maintain the initial states or remember to sync it to the new length of questions if it changes.
const [checked, setChecked] = useState([])
const arrLength = questions.length;
useEffect(() => {
setChecked(() => (
Array(arrLength).fill().map((_, i) => checked[i] || false)
));
}, [arrLength]);
Following your example of mapping over your questions.
You could load those questions in useState and append the check value to them.
Click the Run Code Snippet below to see it working.
const { useState, useEffect } = React;
const App = props => {
const { questions } = props;
const [state, setState] = useState(null);
const onChangeCheckbox = index => event => {
const newState = [...state];
newState[index].checked = !newState[index].checked;
setState(newState);
}
useEffect(() => {
setState(questions.map(i => ({...i, checked: false }) ));
}, [questions]);
// Not loaded yet
if (!state) return <div></div>;
return <div>
{state && state.length > 0 && <div>
{state.map((i, k) => <p key={`question-${k}`}>
<label><input onChange={onChangeCheckbox(k)} type="checkbox" name={i.name} checked={i.checked} /> {i.question}</label>
</p>)}
</div>}
<hr />
<p><small>Debug</small></p>
<pre><code>{JSON.stringify(state, null, ' ')}</code></pre>
</div>;
};
const questions = [
{
name: 'cyborg',
question: 'Are you a cyborg?'
},
{
name: 'earth',
question: 'Do you live on earth?'
},
{
name: 'alien',
question: 'Are you from another planet?'
}
];
ReactDOM.render(<App questions={questions} />, document.querySelector('#root'));
body {
font-family: Arial, sans-serif;
}
pre {
background: #efefef;
padding: 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Use the key param to give each checkbox an identifier so it can be modified.
const [isChecked, setIsChecked] = useState([])
useEffect(() => {
compareList()
})
{ questions.map((q,i) => (
<input
type={q.radio ? "radio" : "checkbox"}
onClick={() => setIsChecked(!isChecked[i])}
value={isChecked[i]}
id={option.id}
value={option.value}
name={`${q.name}`}
key=i
/>
If you use controlled input the solution might be like this:
const initialState = [
{
name: "Check option 1",
checked: false
},
{
name: "Check option 2",
checked: false
},
]
const App = () => {
const [checkboxes, setCheckboxes] = useState(initialState)
const [quizFinished, setQuizFinished] = useState(false)
// the useEffect checks if all questions are completed
useEffect(()=>{
setQuizFinished(checkboxes.reduce((acc, cbx) => acc && cbx.checked, true))
}, [checkboxes])
function handleChange(e) {
setCheckboxes(
checkboxes.map(cbx => cbx.name === e.target.name ? {...cbx, checked:!cbx.checked} : cbx)
)
}
return (
<>
{checkboxes.map(cbx => <Checkbox name={cbx.name} checked={cbx.checked} onChange={handleChange}/>)}
{quizFinished && <button>Finish</button>}
</>
)
}
const Checkbox = ({name, checked, onChange}) => {
return (
<>
<label>
{name}
<input type="checkbox" name={name} checked={checked} onChange={onChange} />
</label>
<br/>
</>
)
}
You can model your state to fit your usecase. For a list of 7 checkboxes you can have an array with 7 values:
const [checked, setChecked] = useState([false, false, false, false, false, false, false])
The when a check box is clicked:
{ questions.map((q, i) => (
<input
type={q.radio ? "radio" : "checkbox"}
onClick={() => setChecked(current => {
const newState = [...current]
newState[i] = !newState[i]
return newState
} )}
value={checked[i]}
id={option.id}
value={option.value}
name={`${q.name}`}
/>
To know if all are checked you can defined a variable:
const areAllChecked = checked.every(Boolean)

react hook how to handle mutiple checkbox

const shoopingList = [{name:'some thing', id:1},{name:'some string', id:4}]
const CurrentLists = ({ shoppingList }) => {
const arr = [...shoppingList]
arr.map((item, index) => {
item.isChecked = false
})
const [checkedItems, setCheckeditems] = useState(arr)
const handleOnChange = (e) => {
const index = e.target.name
const val = e.target.checked
checkedItems[index].isChecked = e.target.checked
setCheckeditems([...checkedItems])
}
return (
<div>
{checkedItems.map((item, index) => {
console.log('item check', item.isChecked)
return (
<CheckBox
key={index}
name={index}
checked={item.isChecked}
text={item.name}
onChange={handleOnChange}
/>
)
})}
</div>
)
}
const CheckBox = ({ checked, onChange, text, className = '', name }) => {
let css = classnames({
activebox: checked,
})
return (
<div className={'CheckBoxComponent ' + className}>
<div className={'checkbox ' + css}>
<input
name={name}
type="checkbox"
onChange={onChange}
/>
{checked && <i className="far fa-check signcheck" />}
</div>
<label>{text}</label>
</div>
)
}
I got some checkboxes. when I click the checkbox, my component doesn't re-render. What's wrong here? I might be using the hook setState wrong.
On every re-render you are basically setting isChecked property to false. Try updating your component like this:
const CurrentLists = ({ shoppingList }) => {
const [checkedItems, setCheckeditems] = useState(shoppingList)
const handleOnChange = useCallback(
(e) => {
const index = e.target.name
let items = [...checkedItems];
items[index].isChecked = e.target.checked;
setCheckeditems(items);
}, [checkedItems]
);
return (
<div>
{checkedItems.map((item, index) => {
console.log('item check', item.isChecked)
return (
<CheckBox
key={index}
name={index}
checked={item.isChecked}
text={item.name}
onChange={handleOnChange}
/>
)
})}
</div>
)
}
You may also notice usage of useCallback. It ensures that your callback is memoized and not created on every re-render - more about it.
In handleOnChange you are mutating the state directly, and because the state reference is not changed React does not re-render. To fix this change the line setCheckeditems(checkedItems) to setCheckeditems([...checkedItems]).
Also in your render, you are rendering shoppingList, but what you need to render is checkedItems

Resources