Kent C. Dodds
Your brain needs this ðŸ§
What changed?
if (navigator.geolocation) {
// use geolocation API
}
Basic content should be accessible to all web browsers.
Basic functionality should be accessible to all web browsers.
Sparse, semantic markup contains all content.
Enhanced layout is provided by externally linked CSS.
Enhanced behavior is provided by externally linked JavaScript.
End-user web browser preferences are respected.
without JS???
<form method="post" action="/todos" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="todoId" value="cl7yrv54a0045j1dz7etu1u9z" />
<input type="hidden" name="complete" value="true" />
<button
type="submit"
name="intent"
value="toggleTodo"
class="toggle"
title="Mark as complete"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="-3 -3 105 105"
>
<circle
cx="50"
cy="50"
r="50"
fill="none"
stroke="#ededed"
stroke-width="3"
></circle>
</svg>
</button>
</form>
What changed?
Dev Experience
User Experience
Even if you are building Figma!
Even if you are building Figma!
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
/>
</deleteFetcher.Form>
</div>
</li>
);
}
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
disabled={isToggling}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
disabled={isToggling}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
disabled={isToggling}
/>
</deleteFetcher.Form>
</div>
</li>
);
}
SPA capabilities + MPA mental model
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
disabled={isToggling}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
disabled={isToggling}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
disabled={isToggling}
/>
</deleteFetcher.Form>
</div>
</li>
);
}
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = isToggling
? toggleFetcher.submission?.formData.get("complete") === "true"
: todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
/>
</deleteFetcher.Form>
</div>
</li>
);
}
switch (intent) {
case "toggleTodo": {
await prisma.todo.update({
where: { id: todoId },
data: { complete: formData.get("complete") === "true" },
});
return new Response("ok");
}
// ...
}
switch (intent) {
case "toggleTodo": {
return json({ error: "Random failure" }, { status: 500 });
await prisma.todo.update({
where: { id: todoId },
data: { complete: formData.get("complete") === "true" },
});
return new Response("ok");
}
// ...
}