Bringing Back
Progressive Enhancement
Kent C. Dodds
Let's wake up
Your brain needs this ðŸ§
What this talk is
- An explanation of Progressive Enhancement
- A demonstrations of what it can do for us
- A mic-drop moment 🎤
What this talk is not
- JS hate-town
Let's
Get
STARTED!
Why was web dev so much easier before?
What changed?
Progressive Enhancement
if (navigator.geolocation) {
// use geolocation API
}
Progressive Enhancement
-
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.
Enabling
Enhancing
<
An example of the status quo
without JS???
Who remembers?
- Built with Remix
- Backend persistence
- User Auth
- Progressively enhanced
- 15% client-side JS
JS does not enable the experience
Built-in Mutations
<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>
Why was web dev so much easier before?
What changed?
Guess the state management lib
The Mental Model
MPA mental model
SPA mental model
>
MPA capabilities
SPA capabilities
<
MPA
mental model
SPA
capabilities
+
Dev Experience
User Experience
There is no application state management with Remix.
Even if you are building Figma!
Even if you are building Figma!
There is no application state management with Remix.
Browser Pending UI
Progressively Unhancement 😬
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>
);
}
Progressive Enhancement 🤩
SPA capabilities + MPA mental model
Optimistic UI
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>
);
}
Optimistic UI
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");
}
// ...
}
Automatic Rollback
Unlimited Optimism 😊
In Review
- Progressive Enhancement is about accessibility
- Remix enables Progressive Enhancement
- Remix simplifies DX and improves UX (MPA + SPA)
- Remix isn't MPA or SPA, it's PESPA (kcd.im/pespa)
- Working without JavaScript is just as much about DX as it is about UX.
You are brilliant
Thank you!
Bringing Back Progressive Enhancement
By Kent C. Dodds
Bringing Back Progressive Enhancement
One fun (sometimes frustrating) part of the web is that your application must run on countless variations of devices with different configurations, screen sizes, and capabilities. Over the years, technology has improved and we've been given some really awesome APIs to enhance our applications to make them more useful despite the uniquely challenging (and awesome) distribution mechanism of the web. Unfortunately, when we use these new features of the web platform to enable our application, we limit our application's usefulness to the trade-offs of those features which has surprisingly negative impacts (even if the device itself supports those features). In this talk, we're going to learn the true scope of Progressive Enhancement and what can be gained by using tools and techniques that allow features of the web to enhance your user's experience rather than enable it.
- 2,517