Skip to content

Commit aec8329

Browse files
authored
refactor(frontend): use Preact Signals (#1107)
`useSignal()` is more ergonomic and performant than `useState()`.
1 parent 04aaea0 commit aec8329

File tree

8 files changed

+102
-112
lines changed

8 files changed

+102
-112
lines changed

frontend/islands/DarkModeToggle.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
2-
import { useEffect, useState } from "preact/hooks";
2+
import { useEffect } from "preact/hooks";
33
import { TbBrightnessUpFilled, TbMoonFilled } from "tb-icons";
4+
import { useSignal } from "@preact/signals";
45

56
export default function DarkModeToggle() {
6-
const [isDark, setIsDark] = useState(false);
7+
const isDark = useSignal(false);
78

89
useEffect(() => {
910
const isDarkStored = localStorage.getItem("darkMode");
@@ -12,14 +13,14 @@ export default function DarkModeToggle() {
1213
const initialDarkMode = isDarkStored === "true" ||
1314
isDarkStored === null && isDarkPreference;
1415

15-
setIsDark(initialDarkMode);
16+
isDark.value = initialDarkMode;
1617
updateTheme(initialDarkMode);
1718

1819
const mediaQuery = globalThis.matchMedia("(prefers-color-scheme: dark)");
1920
const handleChange = () => {
2021
if (localStorage.getItem("darkMode") === null) {
2122
const newDarkMode = mediaQuery.matches;
22-
setIsDark(newDarkMode);
23+
isDark.value = newDarkMode;
2324
updateTheme(newDarkMode);
2425
}
2526
};
@@ -37,8 +38,8 @@ export default function DarkModeToggle() {
3738
}
3839

3940
function toggleDarkMode() {
40-
const newDarkMode = !isDark;
41-
setIsDark(newDarkMode);
41+
const newDarkMode = !isDark.value;
42+
isDark.value = newDarkMode;
4243
updateTheme(newDarkMode);
4344
localStorage.setItem("darkMode", newDarkMode.toString());
4445
}

frontend/islands/PublishingTaskRequeue.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
22
import { api, path } from "../utils/api.ts";
3-
import { useState } from "preact/hooks";
43
import { PublishingTask } from "../utils/api_types.ts";
4+
import { useSignal } from "@preact/signals";
55

66
export default function PublishingTaskRequeue(
77
{ publishingTask }: { publishingTask: PublishingTask },
88
) {
9-
const [processing, setProcessing] = useState(false);
9+
const processing = useSignal(false);
1010

1111
if (
1212
publishingTask.status === "failure" || publishingTask.status === "success"
@@ -19,13 +19,13 @@ export default function PublishingTaskRequeue(
1919
type="button"
2020
disabled={processing}
2121
onClick={() => {
22-
setProcessing(true);
22+
processing.value = true;
2323
api.post(
2424
path`/admin/publishing_tasks/${publishingTask.id}/requeue`,
2525
{},
2626
)
2727
.then((res) => {
28-
setProcessing(false);
28+
processing.value = false;
2929
if (res.ok) {
3030
location.reload();
3131
} else {

frontend/islands/TicketMessageInput.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
2-
import { useState } from "preact/hooks";
32
import { TbCheck, TbClock } from "tb-icons";
43
import {
54
AdminUpdateTicketRequest,
@@ -8,11 +7,12 @@ import {
87
Ticket,
98
} from "../utils/api_types.ts";
109
import { api, path } from "../utils/api.ts";
10+
import { useSignal } from "@preact/signals";
1111

1212
export function TicketMessageInput(
1313
{ ticket, user }: { ticket: Ticket; user: FullUser },
1414
) {
15-
const [message, setMessage] = useState("");
15+
const message = useSignal("");
1616

1717
return (
1818
<form
@@ -23,7 +23,7 @@ export function TicketMessageInput(
2323
api.post(
2424
path`/tickets/${ticket.id}`,
2525
{
26-
message,
26+
message: message.value,
2727
} satisfies NewTicketMessage,
2828
).then((resp) => {
2929
if (resp.ok) {
@@ -40,7 +40,7 @@ export function TicketMessageInput(
4040
value={message}
4141
rows={3}
4242
placeholder="Type your message here..."
43-
onChange={(e) => setMessage(e.currentTarget!.value)}
43+
onChange={(e) => message.value = e.currentTarget!.value}
4444
/>
4545
<div class="flex justify-end gap-4">
4646
<button type="submit" class="button-primary">Send message</button>

frontend/islands/TicketModal.tsx

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
2-
import { useEffect, useId, useRef, useState } from "preact/hooks";
2+
import { useEffect, useId, useRef } from "preact/hooks";
33
import { NewTicket, Ticket, TicketKind, User } from "../utils/api_types.ts";
44
import type { ComponentChildren } from "preact";
55
import { TbLoader2 } from "tb-icons";
66
import { api, path } from "../utils/api.ts";
7+
import { useSignal } from "@preact/signals";
78

89
interface Field {
910
name: string;
@@ -27,11 +28,9 @@ export function TicketModal(
2728
extraMeta?: Record<string, string | undefined>;
2829
},
2930
) {
30-
const [open, setOpen] = useState(false);
31-
const [status, setStatus] = useState<"pending" | "submitting" | "submitted">(
32-
"pending",
33-
);
34-
const [ticket, setTicket] = useState<Ticket | null>(null);
31+
const open = useSignal(false);
32+
const status = useSignal<"pending" | "submitting" | "submitted">("pending");
33+
const ticket = useSignal<Ticket | null>(null);
3534
const buttonRef = useRef<HTMLButtonElement>(null);
3635
const ref = useRef<HTMLFormElement>(null);
3736

@@ -41,17 +40,17 @@ export function TicketModal(
4140
(ref.current && !ref.current.contains(e.target as Element)) &&
4241
(buttonRef.current && !buttonRef.current.contains(e.target as Element))
4342
) {
44-
setOpen(false);
43+
open.value = false;
4544
}
4645
}
4746
document.addEventListener("click", outsideClick);
4847
return () => document.removeEventListener("click", outsideClick);
4948
}, []);
5049

5150
useEffect(() => {
52-
if (!open && status !== "pending") {
51+
if (!open.value && status.value !== "pending") {
5352
setTimeout(() => {
54-
setStatus("pending");
53+
status.value = "pending";
5554
}, 200);
5655
}
5756
}, [open]);
@@ -65,16 +64,16 @@ export function TicketModal(
6564
id={`${prefix}-ticket-modal`}
6665
class={`button-${style}`}
6766
type="button"
68-
onClick={() => setOpen((v) => !v)}
69-
aria-expanded={open ? "true" : "false"}
67+
onClick={() => open.value = !open.value}
68+
aria-expanded={open.value ? "true" : "false"}
7069
disabled={!user}
7170
title={user ? "" : "Please log-in to use this button"}
7271
>
7372
{children}
7473
</button>
7574
<div
7675
class={`fixed top-0 right-0 w-screen h-screen bg-gray-300/40 dark:bg-jsr-gray-950/70 z-[80] flex justify-center items-center overflow-hidden ${
77-
open ? "opacity-100" : "opacity-0 pointer-events-none"
76+
open.value ? "opacity-100" : "opacity-0 pointer-events-none"
7877
} transition`}
7978
aria-labelledby={`${prefix}-ticket-modal`}
8079
role="region"
@@ -83,9 +82,9 @@ export function TicketModal(
8382
<form
8483
ref={ref}
8584
class={`space-y-3 z-[90] rounded border-1.5 border-current dark:border-cyan-700 bg-white dark:bg-jsr-gray-950 shadow min-w-96 ${
86-
status === "pending" ? "w-[40vw]" : ""
85+
status.value === "pending" ? "w-[40vw]" : ""
8786
} max-w-[95vw] max-h-[95vh] px-6 py-4 ${
88-
open ? "translate-y-0" : "translate-y-5"
87+
open.value ? "translate-y-0" : "translate-y-5"
8988
} transition`}
9089
style="--tw-shadow-color: rgba(156,163,175,0.2);"
9190
onSubmit={(e) => {
@@ -109,12 +108,12 @@ export function TicketModal(
109108
meta,
110109
};
111110

112-
setStatus("submitting");
111+
status.value = "submitting";
113112

114113
api.post<Ticket>(path`/tickets`, data).then((res) => {
115114
if (res.ok) {
116-
setStatus("submitted");
117-
setTicket(res.data);
115+
status.value = "submitted";
116+
ticket.value = res.data;
118117
}
119118
});
120119
}}
@@ -123,7 +122,7 @@ export function TicketModal(
123122
New Ticket: {title}
124123
</h2>
125124

126-
{status === "pending"
125+
{status.value === "pending"
127126
? (
128127
<>
129128
<div class="text-sm text-secondary">
@@ -186,7 +185,7 @@ export function TicketModal(
186185
type="button"
187186
class="button-danger"
188187
onClick={() => {
189-
setOpen(false);
188+
open.value = false;
190189
ref.current?.reset();
191190
}}
192191
>
@@ -197,19 +196,21 @@ export function TicketModal(
197196
)
198197
: (
199198
<div class="flex flex-col gap-3 items-center justify-center py-6">
200-
{status === "submitting"
199+
{status.value === "submitting"
201200
? <TbLoader2 class="w-8 h-8 animate-spin" />
202201
: (
203202
<>
204203
<div>
205204
The ticket was submitted. You can view it{" "}
206-
<a href={`/ticket/${ticket!.id}`} class="link">here</a>
205+
<a href={`/ticket/${ticket.value!.id}`} class="link">
206+
here
207+
</a>
207208
</div>
208209
<button
209210
type="button"
210211
class="button-danger"
211212
onClick={() => {
212-
setOpen(false);
213+
open.value = false;
213214
ref.current?.reset();
214215
}}
215216
>

frontend/islands/UserMenu.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
2-
import { useEffect, useId, useRef, useState } from "preact/hooks";
2+
import { useEffect, useId, useRef } from "preact/hooks";
33
import { FullUser } from "../utils/api_types.ts";
44
import { TbArrowRight, TbLogout, TbPlus, TbUser, TbUserCog } from "tb-icons";
5+
import { useSignal } from "@preact/signals";
56

67
const SHARED_ITEM_CLASSES =
78
"flex items-center justify-start gap-2 px-4 py-2.5 focus-visible:ring-2 ring-inset outline-none";
@@ -16,13 +17,13 @@ export function UserMenu({ user, sudo, logoutUrl }: {
1617
sudo: boolean;
1718
logoutUrl: string;
1819
}) {
19-
const [open, setOpen] = useState(false);
20+
const open = useSignal(false);
2021
const ref = useRef<HTMLDivElement>(null);
2122

2223
useEffect(() => {
2324
function outsideClick(e: Event) {
2425
if (ref.current && !ref.current.contains(e.target as Element)) {
25-
setOpen(false);
26+
open.value = false;
2627
}
2728
}
2829
document.addEventListener("click", outsideClick);
@@ -37,8 +38,8 @@ export function UserMenu({ user, sudo, logoutUrl }: {
3738
id={`${prefix}-user-menu`}
3839
class="flex items-center rounded-full focus-visible:ring-2 ring-inset outline-none *:focus-visible:ring-jsr-cyan-400 *:focus-visible:ring-offset-1"
3940
type="button"
40-
onClick={() => setOpen((v) => !v)}
41-
aria-expanded={open ? "true" : "false"}
41+
onClick={() => open.value = !open.value}
42+
aria-expanded={open.value ? "true" : "false"}
4243
>
4344
{(user.inviteCount + user.newerTicketMessagesCount) !== 0 && (
4445
<div class="absolute rounded-full bg-orange-600 border-2 box-content border-white dark:border-jsr-gray-950 -top-0.5 -right-0.5 h-2 w-2" />
@@ -53,7 +54,7 @@ export function UserMenu({ user, sudo, logoutUrl }: {
5354
aria-labelledby={`${prefix}-user-menu`}
5455
role="region"
5556
class={`absolute top-[120%] -right-4 z-[80] rounded border-1.5 border-current bg-white dark:bg-jsr-gray-950 dark:text-gray-200 w-56 shadow overflow-hidden ${
56-
open
57+
open.value
5758
? "opacity-100 translate-y-0"
5859
: "opacity-0 translate-y-5 pointer-events-none"
5960
} transition`}
@@ -101,7 +102,7 @@ export function UserMenu({ user, sudo, logoutUrl }: {
101102
location.reload();
102103
}
103104
}}
104-
tabIndex={open ? undefined : -1}
105+
tabIndex={open.value ? undefined : -1}
105106
class="bg-red-600 hover:bg-red-400 text-white text-sm py-1 px-3 flex justify-between items-center gap-3 rounded-full mt-2"
106107
>
107108
{sudo ? "Disable" : "Enable"} Sudo Mode
@@ -111,15 +112,15 @@ export function UserMenu({ user, sudo, logoutUrl }: {
111112
<div class="divide-y divide-slate-200 dark:divide-jsr-gray-900">
112113
<a
113114
href="/new"
114-
tabIndex={open ? undefined : -1}
115+
tabIndex={open.value ? undefined : -1}
115116
class={`${SHARED_ITEM_CLASSES} font-bold bg-jsr-yellow border-jsr-yellow hover:bg-jsr-yellow-300 hover:border-jsr-cyan-500 focus-visible:bg-jsr-yellow-300 focus-visible:border-jsr-yellow-300 ring-black text-jsr-gray-950`}
116117
>
117118
<TbPlus class="size-5" />
118119
Publish a package
119120
</a>
120121
<a
121122
href={`/user/${user.id}`}
122-
tabIndex={open ? undefined : -1}
123+
tabIndex={open.value ? undefined : -1}
123124
class={`${SHARED_ITEM_CLASSES} ${DEFAULT_ITEM_CLASSES}`}
124125
>
125126
<TbUser class="size-5" />
@@ -128,7 +129,7 @@ export function UserMenu({ user, sudo, logoutUrl }: {
128129
{user.isStaff && (
129130
<a
130131
href="/admin"
131-
tabIndex={open ? undefined : -1}
132+
tabIndex={open.value ? undefined : -1}
132133
class={`${SHARED_ITEM_CLASSES} ${DEFAULT_ITEM_CLASSES}`}
133134
>
134135
<TbUserCog class="size-5" />
@@ -137,7 +138,7 @@ export function UserMenu({ user, sudo, logoutUrl }: {
137138
)}
138139
<a
139140
href={`/logout?redirect=${logoutUrl}`}
140-
tabIndex={open ? undefined : -1}
141+
tabIndex={open.value ? undefined : -1}
141142
class={`${SHARED_ITEM_CLASSES} ${DEFAULT_ITEM_CLASSES}`}
142143
>
143144
<TbLogout class="size-5" />

0 commit comments

Comments
 (0)