Skip to content

Commit

Permalink
Error handling update
Browse files Browse the repository at this point in the history
  • Loading branch information
nawrotw committed Jul 20, 2024
1 parent 4681767 commit 06edda6
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 40 deletions.
24 changes: 12 additions & 12 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState, useRef } from 'react'
import './App.scss'
import { styled, Alert } from "@mui/material";
import { styled } from "@mui/material";
import { AddTodo } from "./components/addTodo/AddTodo.tsx";
import { TodoList } from "./components/todoList/TodoList.tsx";
import { useQuery } from "@tanstack/react-query";
Expand All @@ -9,6 +9,7 @@ import { useTodosMutation } from "./api/todos/useTodosMutation.ts";
import { ActionBar, FilterType } from "./components/actionBar/ActionBar.tsx";
import { colors } from "./styles/colors.ts";
import { Todo } from "./api/todos/Todo.ts";
import { ApiErrorAlert } from "./components/ApiErrorAlert.tsx";

export const Root = styled('div')`
background-color: ${({ theme }) => theme.palette.background.default};
Expand Down Expand Up @@ -39,13 +40,18 @@ const filterMap: Record<FilterType, (todo: Todo) => boolean> = {

function App() {

const { data: todos, isPending, error } = useQuery({ queryKey: ['todos'], queryFn: () => fetchTodos() });
const { data: todos, isPending, error, refetch } = useQuery({ queryKey: ['todos'], queryFn: () => fetchTodos() });

const { addTodo, deleteTodo, updateChecked, updateText, clearCompleted } = useTodosMutation();
const { mutations, addTodo, deleteTodo, updateChecked, updateText, clearCompleted } = useTodosMutation();

const inputRef = useRef<HTMLInputElement>(null);

const [editId, setEditId] = useState<number>();
const [editText, setEditText] = useState<string>("");
const resetEdit = () => {
setEditId(undefined);
setEditText("");
}

const [filter, setFilter] = useState<FilterType>('none');

Expand Down Expand Up @@ -82,18 +88,12 @@ function App() {

const handleSave = (id: number, text: string) => {
updateText({ id, text });
setEditId(undefined);
setEditText("");
};

const handleTextClear = () => {
setEditId(undefined);
setEditText("");
resetEdit();
};

return (
<Root>
{error && <Alert sx={{ m: -1, mb: 1, borderRadius: 0 }} variant="filled" severity="error">{error.message}</Alert>}
<ApiErrorAlert mutations={[...mutations, { error, reset: refetch }]}/>
<Title>todos</Title>
<AddTodo
id={editId}
Expand All @@ -102,7 +102,7 @@ function App() {
onAdd={handleAddTodo}
onSave={handleSave}
onTextChange={setEditText}
onTextClear={handleTextClear}
onTextClear={resetEdit}
/>
<TodoList isPending={isPending} todos={filteredTodos} onToggle={handleToggle} onEdit={handleEdit} onDelete={handleDelete}/>
<ActionBar itemsLeftCount={itemsLeftCount} filter={filter} onFilterChange={setFilter} onClear={clearCompleted}/>
Expand Down
27 changes: 18 additions & 9 deletions src/api/todos/fetchTodos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ export const TODOS_URL = "api/todos";
interface FetchApiProps<Body> {
url?: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE',
body?: Body
body?: Body,
errorMessage?: string,
}

const fetchTodosApi = async <R = void, Body = Record<string, unknown>>(props?: FetchApiProps<Body>): Promise<R> => {

const { url = '', method = 'GET', body } = props || {};
const { url = '', method = 'GET', body, errorMessage } = props || {};

const response = await fetch(TODOS_URL + url, {
headers: {
Expand All @@ -21,14 +22,18 @@ const fetchTodosApi = async <R = void, Body = Record<string, unknown>>(props?: F
body: body ? JSON.stringify(body) : undefined
});

const textResp = await response.text();
const responseBody = textResp ? JSON.parse(textResp) : undefined;
if (!response.ok) {
throw new Error(`Could not fetch todos`);
throw new Error(responseBody?.message || errorMessage || `ServerError`);
}

return await response.json();
return responseBody;
}

export const fetchTodos = () => fetchTodosApi<Todo[]>();
export const fetchTodos = () =>
fetchTodosApi<Todo[]>({
errorMessage: 'Could not fetch todos'
});

export const addTodo = (newTodo: Partial<Todo>) =>
fetchTodosApi({
Expand All @@ -40,24 +45,28 @@ export const deleteTodo = (id: number) =>
fetchTodosApi({
url: `/${id}`,
method: 'DELETE',
errorMessage: 'Delete failed'
});

export const updateTodoText = ({ id, text }: UpdateTodoTextRequest) =>
fetchTodosApi<Todo>({
url: `/${id}/text`,
method: 'PUT',
body: { text }
body: { text },
errorMessage: `Todo '${text}' update failed`
});

export const updateTodoCheck = ({ id, checked }: UpdateTodoCheckedRequest) =>
fetchTodosApi({
url: `/${id}/checked`,
method: 'PUT',
body: { checked }
body: { checked },
errorMessage: `Todo update failed`
});

export const clearCompleted = () =>
fetchTodosApi({
url: '/clear-completed',
method: 'PUT'
method: 'PUT',
errorMessage: `Clearing all todos completion failed`
});
28 changes: 11 additions & 17 deletions src/api/todos/useTodosMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const useTodosMutation = () => {
const addTodoMutation = useMutation({
mutationFn: addTodo,
onMutate: async (newTodo) => {
// Cancel any outgoing re-fetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos']) as Todo[];
queryClient.setQueryData(['todos'], (old: Todo[]) => [...old, newTodo]);
Expand All @@ -24,9 +25,8 @@ export const useTodosMutation = () => {
onError: (_err, _newTodo, context) => {
context && queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => { // Always re-fetch after error or success:
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
// Always re-fetch after error or success:
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const deleteTodoMutation = useMutation({
Expand All @@ -35,9 +35,7 @@ export const useTodosMutation = () => {
await queryClient.cancelQueries({ queryKey: ['todos', id] });
queryClient.setQueryData(['todos'], (old: Todo[]) => old.filter(todo => todo.id !== id));
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});

const updateTextMutation = useMutation({
Expand All @@ -46,21 +44,16 @@ export const useTodosMutation = () => {
await queryClient.cancelQueries({ queryKey: ['todos', id] });
setTodo({ ...getTodo(id), text: text });
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});

const updateCheckedMutation = useMutation({
mutationFn: updateTodoCheck,
onMutate: async ({ id, checked }) => {
// Cancel any outgoing re-fetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries({ queryKey: ['todos', id] });
setTodo({ ...getTodo(id), checked: checked });
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});

const clearCompletedMutate = useMutation({
Expand All @@ -72,17 +65,18 @@ export const useTodosMutation = () => {
(old) => old?.map((todo: Todo) => ({ ...todo, checked: false }))
);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});

})
addTodoMutation.error
addTodoMutation.reset

return {
addTodo: addTodoMutation.mutate,
deleteTodo: deleteTodoMutation.mutate,
updateChecked: updateCheckedMutation.mutate,
updateText: updateTextMutation.mutate,
clearCompleted: clearCompletedMutate.mutate,
mutations: [addTodoMutation, deleteTodoMutation, updateCheckedMutation, updateTextMutation, clearCompletedMutate]
};
}
21 changes: 21 additions & 0 deletions src/components/ApiErrorAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Alert } from "@mui/material";

interface MutationError {
error: Error | null;
reset?: () => void
}

export interface ApiErrorAlertProps {
mutations: MutationError[]
}

export const ApiErrorAlert = ({ mutations }: ApiErrorAlertProps) => {

return mutations
.filter(mutation => mutation.error)
.map((mutation) => (
<Alert sx={{ m: -1, mb: 1, borderRadius: 0 }} variant="filled" severity="error" onClose={() => mutation.reset?.()}>
{mutation.error?.message}
</Alert>
));
}
5 changes: 3 additions & 2 deletions src/components/addTodo/AddTodo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export const AddTodo = forwardRef<HTMLInputElement, AddTodoProps>((props: AddTod
size='small'
sx={{ flex: 1, mr: 0 }}
InputProps={{
sx: {pr: 0},
endAdornment: onTextClear && <InputAdornment position="start">
sx: { pr: 0 },
endAdornment: onTextClear && text.length > 0 && <InputAdornment position="start">
<IconButton onClick={onTextClear} size='small'>
<ClearIcon fontSize='small'/>
</IconButton>
Expand All @@ -53,6 +53,7 @@ export const AddTodo = forwardRef<HTMLInputElement, AddTodoProps>((props: AddTod
type="submit"
sx={{ marginLeft: '5px', width: '20%' }}
variant='contained'
disabled={text.length === 0}
>
{id ? 'Save' : 'Add Todo'}
</Button>
Expand Down

0 comments on commit 06edda6

Please sign in to comment.