Freshで記事読み込み中にローディングを表示する
作成日
更新日
やりたいこと
Deno Fresh のパーシャルを使っているので、それに合わせてローディングして欲しい。
- @preact/signals を使えば良さそう?
- Handlerの中で変えてもその変更は伝わらない。
コレはダメ
サーバーサイドでsignalの値を変えても変更はつたわらない。
// routes/api/start-loading.ts
import { isLoading } from "../../signals/loading.ts";
export const handler: Handlers = {
GET() {
isLoading.value = true; // ← この変更はクライアントには伝わらない
return new Response("OK");
},
};
export default function PostPage({ data }: PageProps) {
const { post } = data;
isLoading.value = true;
return (
<>
</>
)
}
islands
のなかの useEffectで変える必要がある。
これも isLoading.value
は常にfalseになってしまう。islandsのなかじゃないからかな。
export default function PostPage({ data }: PageProps) {
const { post } = data;
return (
<>{isLoading.value ? <Loading /> : <div></div>}
</>
)
}
[サーバー] [クライアント]
┌────────────────┐ ┌────────────────────┐
│ PostPage.tsx │ │ hydrated islands │
│ SSR で HTML 生成│───▶│ signal, useEffect等 │
└────────────────┘ └────────────────────┘
サーバーで isLoading.value
を評価しても「クライアントの状態」とは関係ない。
routes/[slug].tsx
内で isLoading.value
を使っても表示されない
ためす
islands/PostLoading.tsx
とかで変更されるものを渡してみる
import { useEffect, useState } from "preact/hooks";
import { Post } from "../types/post.ts";
import Loading from "../components/molecules/Loading.tsx";
export default function PostLoading({ post }: { post: Post }) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
const timeout = setTimeout(() => {
setIsLoading(false);
}, 2000);
return () => clearTimeout(timeout);
}, [post]);
return isLoading ? <Loading /> : null;
}
ローディングが表示される。 でもコレって実際にはpostの取得が完了したときにローディングを表示しているので謎に待ち時間だけ増えてない?
できたぽい
- signalで isLoading フラグを用意する
islands/RouteEvents.tsx
でページの遷移を検知するislands/PostWrapper.tsx
でisLoadingの値を検知してロード画面の表示/非表示を行う。routes/[slug].tsx
ではPostWrapper.ts
を呼び出す。
signals/loading.ts
import { signal } from "@preact/signals";
export const isLoading = signal(false);
islands/RouteEvents.ts
import { useEffect } from "preact/hooks";
import { isLoading } from "../signals/loading.ts";
export default function RouteEvents() {
useEffect(() => {
let previousPath = location.pathname + location.search;
const handleClick = (e: MouseEvent) => {
const anchor = (e.target as HTMLElement).closest("a");
if (!anchor) return;
const href = anchor.getAttribute("href");
if (
!href ||
anchor.target === "_blank" ||
href.startsWith("http") ||
href.startsWith("#") ||
(href === location.pathname && location.hash !== "")
) return;
isLoading.value = true;
previousPath = location.pathname + location.search;
};
const handlePopState = () => {
const currentPath = location.pathname + location.search;
// ハッシュだけが変わっている(パスは同じ)の場合は無視
if (currentPath === previousPath) return;
isLoading.value = true;
previousPath = currentPath;
};
addEventListener("click", handleClick);
addEventListener("popstate", handlePopState);
return () => {
removeEventListener("click", handleClick);
removeEventListener("popstate", handlePopState);
};
}, []);
return null;
}
<RouteEvents />
は一度だけ設定すれば良い。(再マウントされる必要がないのでkeyはわたさなくていい)
routes/_layout.tsx
import Footer from "../components/Footer.tsx";
import { FreshContext } from "$fresh/server.ts";
import { getCommonPageData } from "../utils/getCommonPageData.ts";
import Navigation from "../components/organisms/Navigation.tsx";
import Header from "../components/Header.tsx";
import { Partial } from "$fresh/runtime.ts";
import RouteEvents from "../islands/RouteEvents.tsx";
export default async function Layout(req: Request, ctx: FreshContext) {
const common = await getCommonPageData();
const postMap = common.postMap;
return (
<>
<div class="flex flex-1 w-full">
{/* 固定ヘッダー */}
<Header />
{/* サイドバー(PCのみ) */}
<aside class="hidden lg:flex flex-col w-72 h-screen fixed pt-40 left-0 z-40">
<Navigation postMap={postMap} />
</aside>
<div class="flex-1 lg:ml-80 flex flex-col min-h-screen justify-between min-w-0">
<div class="flex-1 py-8 min-w-0">
<RouteEvents />
<Partial name="content">
<ctx.Component />
</Partial>
</div>
<div class="order-last">
<Footer />
</div>
</div>
</div>
</>
);
}
islands/PostWrapper.tsx
import { useEffect, useState } from "preact/hooks";
import { isLoading } from "../signals/loading.ts";
import Loading from "../components/molecules/Loading.tsx";
import CoverImage from "../islands/CoverImage.tsx";
import MarkdownContent from "../components/MarkdownContent.tsx";
import Overview from "../components/Overview.tsx";
import { Post } from "../types/post.ts";
export default function PostWrapper({ post }: { post: Post }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
isLoading.value = true;
requestAnimationFrame(() => {
isLoading.value = false;
setMounted(true);
});
}, [post.slug]);
if (!mounted || isLoading.value) {
return <Loading />;
}
return (
<div class="flex flex-row min-w-0 justify-around mx-8">
<main class="flex-1 min-w-0 max-w-3xl border-2 border-gray-600 rounded-xl">
<div class="markdown-body box-border py-6 px-4 rounded-xl">
<h1 class="py-8">{post.title}</h1>
<div class="flex justify-end text-sm text-subText flex-col lg:flex-row">
<div class="mx-2">
公開日 <time>{/* date */}</time>
</div>
<div class="mx-2">
更新日 <time>{/* date */}</time>
</div>
</div>
<CoverImage imageName={post.coverImage ?? ""} />
<MarkdownContent content={post.content} />
</div>
</main>
<aside class="w-1/4 p-4 max-w-xs">
<Overview post={post} />
</aside>
</div>
);
}
routes/[slug].tsx
export default function PostPage({ data }: PageProps) {
const { post } = data;
return (
<>
<Head>
<style dangerouslySetInnerHTML={{ __html: CSS }} />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css"
/>
</Head>
<PostWrapper post={post} />
</>
);
}
これはできてない。jsがないときにはローディング無しでコンテンツを表示してほしい。
さらに修正

公開日
更新日