-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement todo update feature #798
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work 👍
Left some suggestions, check them.
Also, show to user what todos are updating when user clicks the toggle all button.
src/App.tsx
Outdated
useEffect(() => { | ||
let timer: NodeJS.Timeout | undefined; | ||
|
||
if (errorMessage) { | ||
timer = setTimeout(() => { | ||
setErrorMessage(''); | ||
}, 3000); | ||
} | ||
|
||
return () => { | ||
if (timer) { | ||
clearTimeout(timer); | ||
} | ||
}; | ||
}, [errorMessage]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's create a component ErrorNotification
and move this logic and UI to it
src/App.tsx
Outdated
const onToggleAll = async () => { | ||
if (activeTodos.length) { | ||
activeTodos.forEach( | ||
currentTodo => updateTodoHandler(currentTodo, { completed: true }), | ||
); | ||
} else { | ||
todos.forEach( | ||
currentTodo => updateTodoHandler(currentTodo, { completed: false }), | ||
); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to create an array of requests and then handle them with Promise.all
.
Also, show to user which todos are updating, add a loader to them
src/App.tsx
Outdated
<header className="todoapp__header"> | ||
{!!todos.length | ||
&& ( | ||
<button | ||
type="button" | ||
className={classNames( | ||
'todoapp__toggle-all', | ||
{ active: !activeTodos.length }, | ||
)} | ||
data-cy="ToggleAllButton" | ||
onClick={onToggleAll} | ||
/> | ||
)} | ||
|
||
<Form setTempTodo={setTempTodo} /> | ||
</header> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create a component Header
src/App.tsx
Outdated
<div | ||
data-cy="ErrorNotification" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned before, create a component ErrorNotification
src/components/Footer.tsx
Outdated
type Props = { | ||
activeTodos: Todo[], | ||
filter: string, | ||
FilterOption: { [key: string]: string }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have enum FilterOption, use it instead
src/components/TodoItem.tsx
Outdated
export const TodoItem: React.FC<Props> = ( | ||
{ todo }: Props, | ||
) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const TodoItem: React.FC<Props> = ( | |
{ todo }: Props, | |
) => { | |
export const TodoItem: React.FC<Props> = ({ todo }) => { |
You have already set Props type in the generic
src/components/TodoItem.tsx
Outdated
} else if (todoTitle === todo.title) { | ||
setIsEditing(false); | ||
|
||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If todoTitle
is the same we have an infinite spinner as you set it previously setIsItemLoading(true);
and just return in this case
src/components/TodoItem.tsx
Outdated
event.preventDefault(); | ||
setIsItemLoading(true); | ||
|
||
if (todoTitle !== todo.title && todoTitle !== '') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You compare todoTitle !== todo.title
it two times, create a variable.
Also, you don't need to compare with an empty string, just check if todoTitle
exists.
https://developer.mozilla.org/en-US/docs/Glossary/Truthy
src/components/TodoList.tsx
Outdated
{tempTodo | ||
&& <TodoItem todo={tempTodo} />} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{tempTodo | |
&& <TodoItem todo={tempTodo} />} | |
{tempTodo && ( | |
<TodoItem todo={tempTodo} /> | |
)} |
src/components/TodoProvider.tsx
Outdated
const [isLoadingMap, setIsLoadingMap] | ||
= useState<{ [key: number]: boolean } | {}>({}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just store an array of loading todos ids
and it will be a bit easy to manage
…ed other problems
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/App.tsx
Outdated
return todos.filter(({ completed }) => { | ||
switch (filter) { | ||
case FilterOption.Active: | ||
return !completed; | ||
case FilterOption.Completed: | ||
return completed; | ||
case FilterOption.All: | ||
default: | ||
return true; | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this logic to separate helper function
src/App.tsx
Outdated
{!!filteredTodos.length | ||
&& <TodoList todos={filteredTodos} tempTodo={tempTodo} />} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{!!filteredTodos.length | |
&& <TodoList todos={filteredTodos} tempTodo={tempTodo} />} | |
{!!filteredTodos.length && ( | |
<TodoList todos={filteredTodos} tempTodo={tempTodo} /> | |
)} |
src/App.tsx
Outdated
{!!todos.length | ||
&& ( | ||
<Footer | ||
activeTodos={activeTodos} | ||
filter={filter} | ||
setFilter={setFilter} | ||
/> | ||
)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{!!todos.length | |
&& ( | |
<Footer | |
activeTodos={activeTodos} | |
filter={filter} | |
setFilter={setFilter} | |
/> | |
)} | |
{!!todos.length && ( | |
<Footer | |
activeTodos={activeTodos} | |
filter={filter} | |
setFilter={setFilter} | |
/> | |
)} |
src/App.tsx
Outdated
</div> | ||
<ErrorNotification |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
</div> | |
<ErrorNotification | |
</div> | |
<ErrorNotification |
src/components/Footer.tsx
Outdated
const completedTodos = useMemo(() => { | ||
return todos.filter(({ completed }) => completed === true); | ||
}, [todos]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const completedTodos = useMemo(() => { | |
return todos.filter(({ completed }) => completed === true); | |
}, [todos]); | |
const completedTodos = todos.filter(({ completed }) => completed); |
useMemo is useless here
src/components/Footer.tsx
Outdated
.map(({ id }) => deleteTodoHandler(id))); | ||
}; | ||
|
||
const isClearButtonInvisible = completedTodos.length === 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const isClearButtonInvisible = completedTodos.length === 0; | |
const isClearButtonInvisible = !completedTodos.length; |
But you don't really need variable for this
src/components/Header.tsx
Outdated
if (activeTodos.length) { | ||
Promise.all(activeTodos | ||
.map(currentTodo => updateTodoHandler( | ||
currentTodo, | ||
{ completed: true }, | ||
))); | ||
} else { | ||
Promise.all(todos | ||
.map(currentTodo => updateTodoHandler( | ||
currentTodo, | ||
{ completed: false }, | ||
))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DRY. a lot of code repeats, try to refactor this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also add await
before Promise.all
src/components/TodoProvider.tsx
Outdated
const [errorMessage, setErrorMessage] = useState(''); | ||
const [isLoadingTodoIds, setIsLoadingTodoIds] = useState<number[]>([]); | ||
|
||
const addTodoHandler = async ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const addTodoHandler = async ( | |
const handleAddTodo = async ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same all other names
src/components/TodoProvider.tsx
Outdated
|
||
setTodos((currentTodos: Todo[]) => { | ||
return currentTodos.map((currentTodo) => { | ||
return currentTodo.id === updatedTodo.id ? updatedTodo : currentTodo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return currentTodo.id === updatedTodo.id ? updatedTodo : currentTodo; | |
return currentTodo.id === updatedTodo.id | |
? updatedTodo | |
: currentTodo; |
src/components/TodoItem.tsx
Outdated
'is-active': (isLoadingTodoIds.includes(todo.id)) | ||
|| isItemLoading, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to vatiable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM:fire: but needs a few minor fixes:smiley:
src/components/TodoItem.tsx
Outdated
document.addEventListener('keyup', (e) => { | ||
if (e.key === 'Escape') { | ||
setIsEditing(false); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strange situations with this code, It works even if I don't have any todos on the page
I think this article can help you to solve this problem
https://www.freecodecamp.org/news/react-useeffect-absolute-beginners/#:~:text=What%20are%20side%20effects%20in,give%20us%20a%20predictable%20result.
2023-09-29.17-48-55.mov
handleUpdateTodo, | ||
} = useTodo(); | ||
|
||
const onCheckboxChange = async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is just a recomendation:
Component events start with the prefix on
but the function that handles these events should be named with the handle
prefix
src/components/Header.tsx
Outdated
@@ -0,0 +1,114 @@ | |||
/* eslint-disable jsx-a11y/control-has-associated-label */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's fix this eslint issue:smiley:
try to work around aria-label
, I think it can help
src/components/Header.tsx
Outdated
useEffect(() => { | ||
if (todoTitleField.current) { | ||
todoTitleField.current.focus(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useEffect
without a dependency array, is a bad practice
Currently, this useEffect triggers on every Header
render, try to look at how it works on the mate demo.
I think there they focus input only after delete or add todo (after changing the amount of todos)
src/components/TodoItem.tsx
Outdated
? ( | ||
<form | ||
onSubmit={onTodoSave} | ||
onBlur={onTodoSave} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that a lot of tests is not valid, but in your case you have some valid tests failed
For example:
15)
Renaming
Edit Form
on enter before recieved a response
should stay while waiting:
on click
5) should send requests only for not completed todos
src/components/TodoList.tsx
Outdated
{todos.map(todo => { | ||
return ( | ||
<TodoItem | ||
todo={todo} | ||
key={todo.id} | ||
/> | ||
); | ||
})} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove return
keyword here
src/hooks/useTodo.tsx
Outdated
const { | ||
todos, | ||
handleAddTodo, | ||
handleDeleteTodo, | ||
handleUpdateTodo, | ||
errorMessage, | ||
setErrorMessage, | ||
isLoadingTodoIds, | ||
} = useContext(TodoContext); | ||
|
||
return { | ||
todos, | ||
handleAddTodo, | ||
handleDeleteTodo, | ||
handleUpdateTodo, | ||
errorMessage, | ||
setErrorMessage, | ||
isLoadingTodoIds, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it work?
const { | |
todos, | |
handleAddTodo, | |
handleDeleteTodo, | |
handleUpdateTodo, | |
errorMessage, | |
setErrorMessage, | |
isLoadingTodoIds, | |
} = useContext(TodoContext); | |
return { | |
todos, | |
handleAddTodo, | |
handleDeleteTodo, | |
handleUpdateTodo, | |
errorMessage, | |
setErrorMessage, | |
isLoadingTodoIds, | |
}; | |
const todoContextValues = useContext(TodoContext); | |
return todoContextValues |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done 👍
data-cy="ErrorNotification" | ||
className={ | ||
classNames( | ||
'notification is-danger is-light has-text-weight-normal', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'notification is-danger is-light has-text-weight-normal', | |
'notification', 'is-danger', 'is-light', 'has-text-weight-normal', |
<div | ||
data-cy="TodoLoader" | ||
className={classNames( | ||
'modal overlay', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'modal overlay', | |
'modal', 'overlay', |
DEMO LINK