Using functional components and Hooks in React, I'm having trouble moving focus to newly added elements. The shortest way to see this is probably the following component,
function Todos (props) {
const addButton = React.useRef(null)
const [todos, setTodos] = React.useState(Immutable.List([]))
const addTodo = e => {
setTodos(todos.push('Todo text...'))
// AFTER THE TODO IS ADDED HERE IS WHERE I'D LIKE TO
// THROW THE FOCUS TO THE <LI> CONTAINING THE NEW TODO
// THIS WAY A KEYBOARD USER CAN CHOOSE WHAT TO DO WITH
// THE NEWLY ADDED TODO
}
const updateTodo = (index, value) => {
setTodos(todos.set(index, value))
}
const removeTodo = index => {
setTodos(todos.delete(index))
addButton.current.focus()
}
return <div>
<button ref={addButton} onClick={addTodo}>Add todo</button>
<ul>
{todos.map((todo, index) => (
<li tabIndex="0" aria-label={`Todo ${index+1} of ${todos.size}`}>
<input type="text" value={todos[index]} onChange={e => updateTodo(index, e.target.value)}/>
<a onClick={e => removeTodo(index)} href="#">Delete todo</a>
</li>
))}
</ul>
</div>
}
ReactDOM.render(React.createElement(Todos, {}), document.getElementById('app'))
FYI, todos.map realistically would render a Todo component that has the ability to be selected, move up and down with a keyboard, etc… That is why I'm trying to focus the <li> and not the input within (which I realize could be done with the autoFocus attribute.
Ideally, I would be able to call setTodos and then immediately call .focus() on the new todo, but that's not possible because the new todo doesn't exist in the DOM yet because the render hasn't happened.
I think I can work around this by tracking focus via state but that would require capturing onFocus and onBlur and keeping a state variable up to date. This seems risky because focus can move so wildly with a keyboard, mouse, tap, switch, joystick, etc… The window could lose focus…
Use a useEffect that subscribes to updates for todos and will set the focus once that happens.
example:
useEffect(() => {
addButton.current.focus()
}, [todos])
UPDATED ANSWER:
So, you only had a ref on the button. This doesn't give you access to the todo itself to focus it, just the addButton. I've added a currentTodo ref and it will be assigned to the last todo by default. This is just for the default rendering of having one todo and focusing the most recently added one. You'll need to figure out a way to focus the input if you want it for just a delete.
ref={index === todos.length -1 ? currentTodo : null} will assign the ref to the last item in the index, otherwise the ref is null
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function Todos(props) {
const currentTodo = React.useRef(null)
const addButton = React.useRef(null)
const [todos, setTodos] = useState([])
useEffect(() => {
const newTodos = [...todos];
newTodos.push('Todo text...');
setTodos(newTodos);
// event listener for click
document.addEventListener('mousedown', handleClick);
// removal of event listener on unmount
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, []);
const handleClick = event => {
// if there's a currentTodo and a current addButton ref
if(currentTodo.current && addButton.current){
// if the event target was the addButton ref (they clicked addTodo)
if(event.target === addButton.current) {
// select the last todo (presumably the latest)
currentTodo.current.querySelector('input').select();
}
}
}
const addTodo = e => {
const newTodo = [...todos];
newTodo.push('New text...');
setTodos(newTodo);
}
// this is for if you wanted to focus the last on every state change
// useEffect(() => {
// // if the currentTodo ref is set
// if(currentTodo.current) {
// console.log('input', currentTodo.current.querySelector('input'));
// currentTodo.current.querySelector('input').select();
// }
// }, [todos])
const updateTodo = (index, value) => {
setTodos(todos.set(index, value))
}
const removeTodo = index => {
setTodos(todos.delete(index))
currentTodo.current.focus()
}
return <div>
<button onClick={addTodo} ref={addButton}>Add todo</button>
<ul>
{todos.length > 0 && todos.map((todo, index) => (
<li tabIndex="0" aria-label={`Todo ${index + 1} of ${todos.length}`} key={index} ref={index === todos.length -1 ? currentTodo : null}>
<input type="text" value={todo} onChange={e => updateTodo(index, e.target.value)} />
<a onClick={e => removeTodo(index)} href="#">Delete todo</a>
</li>
))}
</ul>
</div>
}
ReactDOM.render(React.createElement(Todos, {}), document.getElementById('root'))
Just simply wrap the focus() call in a setTimeout
setTimeout(() => {
addButton.current.focus()
})
Related
I was watching a tutorial on how to make todos, though my main focus was local storage use.
But when he made the delete button then I was a bit confused, the code below shows how he did it but I am not getting it.
Can anyone explain that I tried using the splice method to remove items from the array but I am not able to remove the items from the page?
Can you also suggest what should I do after using splice to return the array on the page?
Below is the code,
import "./styles.css";
import { useState, useEffect } from 'react'
import Todoform from './TodoForm'
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (e) => {
// TODO: items.splice(e-1, 1);
// Is there any other way I can do the below thing .i.e
// to remove todos from page.
// this is from tutorial
setitems((e1)=>{
return e1.filter((er , index)=>{
return index!=e-1;
})
})
}
return (
<>
<div className='display_info'>
<h1>TODO LIST</h1>
<br />
<input onChange={itemevent} value={list} type="text" name="" id="" />
<br />
<button onClick={listofitem} >Add </button>
<ul>
{
items.map((e, index) => {
index++;
return (
<>
<Todoform onSelect={deleteItems} id={index} key={index} index={index} text={e} />
</>
)
})
}
</ul>
</div>
</>
)
}
And this is the TodoForm in this code above,
import React from 'react'
export default function Todoform(props) {
const { text, index } = props;
return (
<>
<div key={index} >
{index}. {text}
<button onClick={() => {
props.onSelect(index)
}} className="delete">remove</button>
</div>
</>
)
}
Here is the codeSandbox link
https://codesandbox.io/s/old-wood-cbnq86?file=/src/TodoForm.jsx:0-317
I think one issue with your code example is that you don't delete the todo entry from localStorage but only from the components state.
You might wanna keep localStorage in sync with the components state by using Reacts useEffect hook (React Docs) and use Array.splice in order to remove certain array elements by their index (Array.splice docs).
// ..
export default function App() {
const [list, setlist] = useState("");
const [items, setitems] = useState([])
/* As this `useEffect` has an empty dependency array (the 2nd parameter), it gets called only once (after first render).
It initially retrieves the data from localStorage and pushes it to the `todos` state. */
useEffect(() => {
const todos = JSON.parse(localStorage.getItem("notes"));
setitems(todos);
}, [])
/* This `useEffect` depends on the `items` state. That means whenever `items` change, this hook gets re-run.
In here, we set sync localStorage to the current `notes` state. */
useEffect(() => {
localStorage.setItem("notes", JSON.stringify(items));
}, [items])
const itemevent = (e) => {
setlist(e.target.value);
}
const listofitem = () => {
setitems((e) => {
return [...e , list];
})
}
const deleteItems = (index) => {
// This removes one (2nd parameter) element(s) from array `items` on index `index`
const newItems = items.splice(index, 1)
setitems(newItems)
}
return (
<>
{/* ... */}
</>
)
}
There are multiple ways to remove an item from a list in JS, your version of splicing the last index is correct too and it is able to remove the last item. What it can't do is update your state.
His code is doing two things at the same time: Removing the last item of the Todo array and then, setting the resulted array in the state which updates the todo list.
So, change your code as
const deleteItems = (e) => {
let newItems = [...items];
newItems.splice(e-1, 1);
setitems(newItems);
}
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.
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'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 have a react js application where I render a list of items. When a user click on an item and after that clicks outside in console.log(id, "checked id"); I should see only that item where user clicked before.
import "./styles.css";
import React, { useEffect, useState } from "react";
const Element = ({ id, setId }) => {
const handleClick = (e) => {
const condition = e.target.className.includes("test");
if (!condition) {
setId(id);
}
};
console.log(id, "checked id");
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, []);
return (
<div
className={"test"}
onClick={() => {
setId(id);
}}
>
hello {id}
</div>
);
};
export default function App() {
const [idSelect, setIdSelect] = useState(0);
return (
<div className="App">
{[0, 1, 2].map((el) => {
return <Element setId={setIdSelect} key={el} id={el} />;
})}
</div>
);
}
Basically I should trigger only that element where user previously clicked. EX: user clicks on hello 0 item and after that clicks outside, in console they should see only console.log(0, "checked id");, meaning that only that component is triggered. Issue: at the moment when user click outside, all the components renders, but I need only where user clicked.
demo: https://codesandbox.io/s/compassionate-einstein-9v1ws?file=/src/App.js:0-821 Question: How to solve my issue?
First you need to move the console.log to the useEffect to control when it will be executed, since you wanted to execute when the document is clicked them do it the clickHandle.
useEffect(() => {
const handleClick = (e) => {
const condition = e.target.className.includes("test");
if (!condition && isSelected) {
console.log(id, "checked id");
setId(id); // You don't need this, it's already set to current id.
}
};
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, [setId, id, isSelected]);
To make sure only the selected Element logs to console i added the attribute isSelected to handle that.
isSelected={idSelect === el}
This should solve the problem, but just to be safe i added stopPropagation to the div click to make sure it doesn't trigger the document click.
onClick={(e) => {
e.stopPropagation();
setId(id);
}}
You can check this sandbox i modified here.
It seems like we have many attributes right, i believe a good solution for that is move the display event to the parent tag, like this.
export default function App() {
const [idSelect, setIdSelect] = useState(0);
useEffect(() => {
const handleClick = (e) => {
console.log(idSelect, "checked id");
}
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
});
return (
<div className="App">
{[0, 1, 2].map((el) => {
return (
<Element
setId={setIdSelect}
key={el}
id={el}
/>
);
})}
</div>
);
}
You can also replace setId and id attributes with a single click event handler like () => setId(id).
If you want the function to be executed as well when the selected id changes, you can use useEffect like this.
useEffect(() => {
console.log(idSelect, "checked id");
}, [idSelect]);
I suggest moving the console.log(idSelect, "checked id"); to a function and call it at both events.