fresh v1.3がリリースされました。

この記事では主な変更点などについて解説します。

非同期Routeコンポーネント

非同期Routeコンポーネントがサポートされました。

例えばデータベースや外部のAPIなどから非同期に取得したデータをRouteコンポーネントに渡すためには、今までは以下のようにhandlerを定義する必要がありました。

// routes/users/[id].tsx
import type { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers<Data> = {
  async GET(req, ctx) {
    const user = await findUserByID(ctx.params.id);
    if (user == null) {
      return ctx.renderNotFound();
    }
    const resp = await ctx.render(user);
    return resp;
  },
};

export default async function User(props: PageProps<User>) {
  return <UserDetail user={props.data} />;
}

非同期Routeコンポーネントを利用することで、handlerを記述せずに非同期でのデータの取得とコンポーネントのレンダリングが行えるようになります。

// routes/users/[id].tsx
import type { RouteContext } from "$fresh/server.ts";

export default async function User(
  req: Request,
  ctx: RouteContext,
) {
  const user = await findUserByID(ctx.params.id);
  if (user == null) {
    return ctx.renderNotFound();
  }
  return <UserDetail user={user} />;
}

上記のようにasync関数がdefault exportされている場合、その関数は非同期Routeコンポーネントとみなされます。

非同期RouteコンポーネントはRequestRouteContextを引数として受け取り、vnodeまたはResponseのいずれかを返すことができます。


https://github.com/denoland/fresh/pull/1388

プラグインからのRouteとMiddlewareの注入がサポート

Pluginroutesmiddlewaresプロパティがサポートされました。

これらを指定することで、プラグインによってRouteやMiddlewareを注入できます。

import type { Plugin } from "$fresh/server.ts";

interface Options {
  path: string;
}

export default function samplePlugin(options: Options): Plugin {
  return {
    name: "samplePlugin",

    // 全てのリクエスト(`path: "/"`)に対して実行されるMiddlewareを登録します。
    middlewares: [{
      middleware: { handler: loggingMiddleware },
      path: "/",
    }],

    // `options.path`にアクセスされた際に、`component`がレンダリングされます。
    routes: [{
      path: options.path,
      component: () => <div>hello</div>,
    }],
  };
}

https://github.com/denoland/fresh/pull/1455

Error Boundaryがサポート

PreactのError Boundaryがサポートされました。

// components/ErrorBoundary.tsx
import { Component } from "preact";

import ErrorView from "./ErrorView.tsx";

export default class ErrorBoundary extends Component {
  state = { error: null } as { error: Error | null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  componentDidCatch(error) {
    console.error(error.message);
  }

  render() {
    if (this.state.error) {
      return <ErrorView error={this.state.error} />;
    }

    return <>{this.props.children}</>;
  }
}

Error Boundaryを利用することで、コンポーネントのレンダリング時に発生したエラーを補足できます。

// routes/index.tsx
import Main from "../components/Main.tsx";
import ErrorBoundary from "../components/ErrorBoundary.tsx";

export default function Index() {
  return (
    <ErrorBoundary>
      <Main />
    </ErrorBoundary>
  );
}

Islandコンポーネントに関する改善

単一ファイルでの複数コンポーネントのexportがサポート

islands/ディレクトリ内のファイルで複数のコンポーネントをexportできるようになりました。(元々はdefault exportされたコンポーネントのみが許可されていました。)

// islands/foobar.tsx
export function Foo() {
  return <div>foo</div>;
}

export function Bar() {
  return <div>bar</div>;
}

https://github.com/denoland/fresh/pull/1397

propsでのbigint型のサポート

bigint型の値をIslandコンポーネントのpropsとして渡せるようになりました。

// islands/Counter.tsx
import { useState } from "preact/hooks";

interface Props {
  count: bigint;
}

export default function Counter(props: Props) {
  const [count, setCount] = useState(props.count);
  return (
    <div class="flex gap-2 w-full">
      <p class="flex-grow-1 font-bold text-xl">{count}</p>
      <button onClick={() => setCount((count) => count - 1n)}>-1</button>
      <button onClick={() => setCount((count) => count + 1n)}>+1</button>
    </div>
  );
}
<Counter count={123n} />

https://github.com/denoland/fresh/pull/1317

Server

デフォルトでDeno.serveが使用されるように

Deno v1.35での安定化に合わせて、デフォルトでDeno.serveがFreshの内部で使われるようになりました。

これにより、パフォーマンスの改善などが期待されそうです。


https://github.com/denoland/fresh/pull/1427

HandlerContext.renderNotFoundでパラメータがサポート

HandlerContext.renderNotFoundに引数を渡せるようになりました。

// routes/users/[id].tsx
import type { Handlers, PageProps } from "$fresh/server.ts";

import type { Data } from "../_404.tsx";

export const handler: Handlers = {
  async GET(req, ctx) {
    const user = await getUser(ctx.params.id);
    if (user == null) {
      const data: Data = { message: "Not Found" };
      return ctx.renderNotFound(data);
    }
    return ctx.render(user);
  },
};

export default function Index(props: PageProps<User>) {
  return <UserDetail user={props.data} />;
}

引数に渡した値は、_404.tsxprops.dataとして受け取ることができます。

// routes/_404.tsx
import { UnknownPageProps } from "$fresh/server.ts";

export interface Data {
  message: string;
}

export default function NotFoundPage(props: UnknownPageProps<Data>) {
  return <p>{props.data.message}</p>;
}

https://github.com/denoland/fresh/pull/1310

createHandlerで作ったハンドラの第2引数が省略可能に

createHandlerから返されるハンドラの第2引数の型がServeHandlerInfoという新しい型へ変更されています。

これに合わせて、ハンドラの第2引数を省略できるようになりました。

import manifest from "./fresh.gen.ts";
import { createHandler } from "$fresh/server.ts";

import twindv1 from "$fresh/plugins/twindv1.ts";
import twindConfig from "./twind.config.ts";

const handler = await createHandler(manifest, { plugins: [twindv1(twindConfig)] });
// 第2引数を省略できます。
const res = await handler(new Request("http://localhost:8000/"));

Routes

ミドルウェアでRouteパラメータを受け取れるように

MiddlewareHandlerContextparamsプロパティが追加されました。

これにより、ミドルウェアでRouteパラメータを受け取れるように改善されています。

// routes /_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";

export async function handler(
  _req: Request,
  ctx: MiddlewareHandlerContext,
) {
  doSomethingWithParams(ctx.params);
  const resp = await ctx.next();
  return resp;
}

https://github.com/denoland/fresh/pull/1314

Routeコンポーネントでstateがサポート

PageProps及びAppPropsstateが追加されています。

これにより、各RouteコンポーネントなどがMiddlewareで設定されたstateを参照できるようになります

// types/route_state.ts
export interface State {
  date: Date;
}

例えば、以下のようにMiddlewareでstateに値を設定したとします。

// routes /_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
import type { State } from "../types/route_state.ts";

export async function handler(
  _req: Request,
  ctx: MiddlewareHandlerContext<State>,
) {
  ctx.state.date = new Date();
  const resp = await ctx.next();
  return resp;
}

stateに設定された値はRouteコンポーネントから参照できます。

// routes/index.tsx
import type { PageProps } from "$fresh/server.ts";
import type { State } from "../types/route_state.ts";

export default function Index(props: PageProps<unknown, State>) {
  return (
    <div>{props.state.date.toISOString()}</div>
  );
}

https://github.com/denoland/fresh/pull/1264

重複するRouteの検出

重複したRouteを定義しようとした際に、エラーが発生するように挙動が改善されました。

例えば、routes/user.tsroutes/user.tsxが同時に存在する場合はエラーが発生します。


https://github.com/denoland/fresh/pull/1410

Freshの自動アップデート

devサーバの起動時にFreshの最新バージョンがリリースされていないか自動でチェックが行われるようになりました。 (1日に1回)

もしFreshの最新バージョンがリリースされていたら、コンソールにメッセージが表示されます。

この機能を無効化したい場合は、FRESH_NO_UPDATE_CHECK環境変数にtrueを設定する必要があります。

バージョンチェック用のキャッシュファイルは、Linuxだと$XDG_CACHE_HOME/latest.json, Macだと$HOME/Library/Caches/latest.jsonに作られるようです。


https://github.com/denoland/fresh/pull/1444

バグ修正

  • handlerが同期的に例外を投げた際に、_500.tsxが描画されない問題が修正されました
  • Middlewareがドキュメントに記述されている順序通りに実行されるように修正されました。 (#1090)
  • router.trailingSlashオプションが有効化されている状態でハッシュやクエリパラメータが付与されたURLが渡された際に、意図した位置に/が付与されない問題が修正されました。

参考