Repository navigation
Expand file tree
/
Copy pathLikeButton.jsx
More file actions
105 lines (95 loc) · 2.79 KB
/
Copy pathLikeButton.jsx
File metadata and controls
105 lines (95 loc) · 2.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { useEffect, useState } from "react";
const faultMode =
new URLSearchParams(window.location.search).get("fault") ??
import.meta.env.VITE_DEMO_DEFAULT_FAULT ??
null;
export function LikeButton() {
const [liked, setLiked] = useState(false);
const [phase, setPhase] = useState("loading");
useEffect(() => {
const controller = new AbortController();
async function loadServerTruth() {
try {
const response = await fetch("/api/like", {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`GET /api/like returned ${response.status}`);
}
const state = await response.json();
setLiked(state.liked);
setPhase("ready");
} catch (error) {
if (error.name !== "AbortError") {
setPhase("load-error");
}
}
}
loadServerTruth();
return () => controller.abort();
}, []);
async function likeArticle() {
const previousLiked = liked;
setLiked(true);
if (faultMode === "omit-post") {
setPhase("omitted");
return;
}
setPhase("saving");
try {
const response = await fetch("/api/like", {
method: "POST",
headers: {
"content-type": "application/json",
...(faultMode === "reject-post"
? { "x-demo-fault": "reject" }
: {}),
},
body: JSON.stringify({ liked: true }),
});
if (!response.ok) {
throw new Error(`POST /api/like returned ${response.status}`);
}
setPhase("saved");
} catch {
setLiked(previousLiked);
setPhase("write-error");
}
}
const status = {
loading: "Loading server truth…",
ready: liked
? "Ready. The server says this article is liked."
: "Ready. The server says this article is not liked.",
saving: "Showing optimistic success while the write is pending…",
saved: "Saved on server.",
omitted: "Optimistic UI updated; POST omitted by demo fault.",
"load-error": "Could not load server truth.",
"write-error": "Ready after rollback.",
}[phase];
return (
<section className="demo-card" aria-labelledby="demo-title">
<div>
<p className="card-label">Article reaction</p>
<h2 id="demo-title">A tiny optimistic write</h2>
</div>
<button
type="button"
aria-pressed={liked}
disabled={["loading", "saving", "load-error"].includes(phase)}
onClick={likeArticle}
>
<span aria-hidden="true">♥</span>
Like article
</button>
<p className="status" role="status">
{status}
</p>
{phase === "write-error" ? (
<p className="alert" role="alert">
Write failed. Optimistic state rolled back.
</p>
) : null}
</section>
);
}