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.
Related
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
The problem is that my child component is not being re-rendered after I update name state using chrome.storage.sync.get.
So even though name="John" radio button is not checked.
storage.ts:
export function getName(): Promise<nameInterface> {
return new Promise((resolve) => {
chrome.storage.sync.get("name", (response: nameInterface) => {
resolve(response)
})
})
}
popup.tsx:
const [name, setName] = useState<string>()
useEffect(() => {
getName().then((storedName) => {
setName(storedName)
})
}, [])
return (
<div>
<RadioButton
name={name}
></RadioButton>
)
child.tsx:
const RadioButton: React.FC<AppProps> = ({ name}) => {
const [buttonChecked, setButtonChecked] = useState<string>(name)
return (
<input type="radio" checked={buttonChecked==="John"} />
)
}
It's re-rendering.
But what happens when this component re-renders? Nothing much. It already captured state from props the first time it rendered, and internally it only ever uses that state, and nothing ever changes that state. Since the state never changed, nothing in the UI changed.
If you want the component to use the prop instead of maintain internal state, get rid of the internal state:
const RadioButton: React.FC<AppProps> = ({name}) => {
return (
<input type="radio" checked={name==="John"} />
)
}
I'm trying to code To do list. I have an array that includes all the tasks as an objects.
every task has an property called "checked" to indicate me if one has checked this task.
When a task got checked, i have onClicked function that changes the "checked" property of the specified task in the array, and in this point I have an error of "too many re-renders".
whould like your help..
import { useEffect, useState } from "react";
import react from "react";
import "./styles.css";
export default function ToDoList() {
const [allTasks, setAllTasks] = useState([]);
const [newTask, setNewTask] = useState({ value: "" });
const [currentIndex, setCurrentIndex] = useState(0);
const handleChanged = ({ target }) => {
setNewTask({
value: target.value,
id: currentIndex,
checked: false
});
};
function onClicked() {
if (newTask.value === "") return;
setAllTasks((prev) => {
return [newTask, ...prev];
});
setNewTask({ value: "" });
setCurrentIndex((prevCurrentIndex) => prevCurrentIndex + 1);
}
const handleKeyPress = (event) => {
if (event.keyCode === 13) {
onClicked();
}
};
const onCheck = (i) => {
const task = { ...allTasks[i] };
task.checked = !task.checked;
const taskState = [...allTasks];
taskState[i] = task;
setAllTasks(taskState);
};
return (
<div>
<h1>
<u>
<b>To Do List</b>
</u>
</h1>
<input
value={newTask.value}
onChange={handleChanged}
onKeyDown={handleKeyPress}
/>
<button onClick={onClicked} />
<h1>{newTask.value}</h1>
<lo>
{allTasks.map((item, i) => (
<li>
{item.checked ? (
<input type="checkbox" checked={true} onClick={onCheck(i)} />
) : (
<input type="checkbox" checked={false} onClick={onCheck(i)} />
)}
{item.value}
</li>
))}
</lo>
</div>
);
}
Immediately upon render you call the onCheck function (using the result of the function, which is undefined, as the actual click handler):
onClick={onCheck(i)}
Which updates state, which triggers a re-render, which calls the function, which updates state, etc., etc.
Don't call onCheck immediately during the render. Call it when the user clicks on the element by wrapping it in a function:
onClick={() => onCheck(i)}
This sets the function itself as the click handler and only invokes that function when that even occurs.
I am working on a component where the user searches a term and it is returned to them through a filter. I am using useContext hook to pass data from db via axios. I would like to use the button in the CompSearch component to render the results rather than having them show up on a keystroke. My question is how do I render the results via button click?
Here is the code
Follow these steps to achieve that.
Change the input element into an uncontrolled component.
Get the value using reference in React.
import React, { useContext, useRef, useState } from "react";
import CompanyInfoList from "./CompanyInfoList";
import { CompListContext } from "./CompListContext";
const CompSerach = () => {
const [company, setCompany] = useContext(CompListContext);
const [searchField, setSearchField] = useState("");
const [searchShow, setSearchShow] = useState(false);
const filtered = company.filter((res) => {
return res.company.toLowerCase().includes(searchField.toLowerCase());
});
const inputRef = useRef(null); // 1. Create the ref
const handleClick = () => {
const val = inputRef.current.value; // 3. Get the value
setSearchField(val);
if (val === "") {
setSearchShow(false);
} else {
setSearchShow(true);
}
};
const searchList = () => {
if (searchShow) {
return <CompanyInfoList filtered={filtered} />;
}
};
return (
<>
<div>
<em>
NOTE: if you search "ABC" or "EFGH" you will see results - my goal is
to see those results after clicking button
</em>
<br />
<input
type="search"
placeholder="search Company Title"
ref={inputRef} {/* 2. Assign the ref to the Input */}
/>
<button onClick={handleClick}>Enter</button>
</div>
{searchList()}
</>
);
};
export default CompSerach;
https://codesandbox.io/s/show-on-button-click-68157003-rot6o?file=/src/TestTwo/CompSearch.js
Let me know if you need further support.
const handleChange = (e) => {
setSearchField(e.target.value);
if (e.target.value === "") {
setSearchShow(false);
} else {
setSearchShow(true);
}
**setCompany(e.target.value);**
};
i think your question is similar with autocomplete.
I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.
Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.
A simplified example would be this :
import React, { useState } from 'react'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = props => {
const { object, onChange } = props
return (
<li>
<Input value={object.name} onChange={onChange('name')} />
</li>
)
}
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = id => key => value => {
const newObjects = objects.map(obj => {
if (obj.id === id) {
return {
...obj,
[key]: value
}
}
return obj
})
setObjects(newObjects)
}
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
))
}
</ul>
)
}
export default Objects
So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.
I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.
It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.
Any ideas ?
I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state.
This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.
handleChangeName = (e) => {
this.setState({ name: e.target.value }, () => this.change());
}
change = debounce(() => {
const { index, onChange } = this.props;
const { name } = this.state;
onChange(index, { name });
}, 300);
This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.
You have to use the functional form of setState:
setState((prevState) => {
// ACCESS prevState
return someNewState;
});
You'll be able to access the current state value (prevState) while updating it.
Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.
Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.
EXAMPLE IN SNIPPET BELOW
const InputField = React.memo((props) => {
console.log('Rendering InputField '+ props.index + '...');
return(
<div>
<input
type='text'
value={props.value}
onChange={()=>
props.handleChange(event.target.value,props.index)
}
/>
</div>
);
});
function App() {
console.log('Rendering App...');
const [inputValues,setInputValues] = React.useState(
['0','1','2']
);
const handleChange = React.useCallback((newValue,index)=>{
setInputValues((prevState)=>{
const aux = Array.from(prevState);
aux[index] = newValue;
return aux;
});
},[]);
const inputItems = inputValues.map((item,index) =>
<InputField
value={item}
index={index}
handleChange={handleChange}
/>
);
return(
<div>
{inputItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Okay, so it seems that debounce works if it's wrapped in useCallback
Not sure why it doesn't seem to be necessary to pass newObject as a dependency in the updateParent function though.
So to make this work I had to make the following changes :
First, useCallback in the parent and change it to take the whole object instead of being responsible for updating the keys.
Then update the ObjectEditor to have its own state and handle the change to the keys.
And wrap the onChange handler that will update the parent in the debounce
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = React.memo(props => {
const { initialObject, onChange } = props
const [object, setObject] = useState(initialObject)
const updateParent = useCallback(debounce((newObject) => {
onChange(newObject)
}, 500), [onChange])
// synchronize the object if it's changed in the parent
useEffect(() => {
setObject(initialObject)
}, [initialObject])
const handleChange = key => value => {
const newObject = {
...object,
[key]: value
}
setObject(newObject)
updateParent(newObject)
}
return (
<li>
<Input value={object.name} onChange={handleChange('name')} />
</li>
)
})
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = useCallback(newObj => {
const newObjects = objects.map(obj => {
if (newObj.id === id) {
return newObj
}
return obj
})
setObjects(newObjects)
}, [objects])
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} initialObject={obj} onChange={handleObjectChange} />
))
}
</ul>
)
}
export default Objects