Skip to main content
Fresh lemon

Fresh 1.3 – Simplified Route Components and More

A mere month has passed since we released Fresh 1.2 into the wild and we’re already back with another release! We’re planning to release new minor versions of Fresh on a monthly cadence going forward.

This cycle contained lots of incredible PRs from the community, which is nothing short of amazing! The documentation has been expanded and improved, many bugs fixed and new features added. Thank you to everyone who helped in making this release possible. But enough talk, let’s look at all the improvements we made to Fresh.

Async Route Components

We heard a lot of feedback that passing data from a route handler to the page’s component requires a bit of annoying boilerplate. To ensure type-safety, you’d always have to create an interface for the component’s props, pass that to the Handlers type as a generic and use that in the component definition. That’s quite a lot of steps!

// previous Fresh versions
interface Data {
  foo: number;
}

export const handler: Handlers<Data> = {
  async GET(req, ctx) {
    const value = await loadFooValue();
    ctx.render({ foo: value });
  },
};

export default function MyPage(props: PageProps<Data>) {
  return <p>foo is: {props.data.foo}</p>;
}

Since the GET handler is very often highly coupled to the component it will render, the obvious question is why we don’t just merge the two together? And that’s exactly what we did. In Fresh 1.3 we can simplify the snippet from before significantly:

export default async function MyPage(req: Request, ctx: RouteContext) {
  const value = await loadFooValue();
  return <p>foo is: {value}</p>;
}

Since both the handler and the component are in the same function there is no need to declare an intermediate interface to pass data between the two; you can just pass the data directly.

But don’t worry, there is no need to rewrite all your routes. In fact we’re not fans of having to rewrite code ourselves. The new way is nothing more than an additional option and can make simple routes a bit easier to write. It is not a requirement to use async routes.

When you’re responding to other HTTP verbs like POST you might need a handler anyway:

export const handler: Handlers<{}> = {
  POST(req) {
    // ... do something here
  },
};

export default async function MyPage(req: Request, ctx: RouteContext) {
  const value = await loadFooValue();
  return <p>foo is: {value}</p>;
}

Moreover, the existing way of rendering routes from a handler via ctx.render() and a separate component function will continue to work and makes it possible to test rendering and the data retrieval mechanisms in isolation.

Adding routes and/or middlewares from plugins

Plugins are quite powerful in extending Fresh’s built-in capabilities. With Fresh 1.3 they can inject virtual routes and middlewares. This is especially useful for plugins adding development specific routes or admin dashboards.

function myPlugin() {
  return {
    name: "my-plugin",
    middlewares: [
      {
        middleware: { handler: () => new Response("Hello!") },
        path: "/hello",
      },
    ],
    routes: [
      {
        path: "/admin/hello",
        component: () => <p>Hello from /admin/hello</p>,
      },
    ],
  };
}

Thanks to Reed von Redwitz and iccee0 for the contribution.

500 error template fallback

As much as we programmers try to account for every scenario, there often remain unexpected cases in which an error occurs. We’ve simplified error handling a bit and Fresh will now automatically render the _500.tsx template as a fallback when an error is thrown in a route handler.

export const handler = (req: Request, ctx: HandlerContext): Response => {
  // Fresh will catch this errors and render the 500 error template
  throw new Error("Catch me if you can");
};

Thanks to Kamil Ogórek for adding this.

Error Boundaries

Although a little rarer than errors in handlers, errors can also be thrown during rendering. For this reason we added basic support for error boundaries. When Preact detects that a class component that has a componentDidCatch() method or a static getDerivedStateFromError method, the component is treated as an error boundary. When an error is thrown during rendering, those components can catch the error and render a fallback UI:

class ErrorBoundary extends Component {
  state = { error: null };

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

  render() {
    return this.state.error
      ? this.props.fallback(this.state.error)
      : this.props.children;
  }
}

// Usage:
<ErrorBoundary fallback={(error) => <p>Error happened: {error.message}</p>}>
  <SomeComponentThatThrows />
</ErrorBoundary>;

This is useful for when you’re dealing with highly dynamic data and want to render a fallback UI instead of rendering the 500 error page. Whilst the changes in Fresh 1.3 lay the foundation for catching rendering errors, we’re already thinking of ways we could make the API feel more ingrained with Fresh itself.

Export multiple islands in the same file

In previous Fresh versions every island was expected to live in its own file and be exported via a default export. Every island file is treated as its own entry point and shipped in a separate JavaScript file to the browser. This limitation has been removed and you can now export as many islands in a single file as you want.

// ./islands/MyIsland.tsx
// Export multiple islands in Fresh 1.3
export default function SayHello() {
  // ...island code here
}

export function OtherIsland() {
  // ...island code here
}

export function AndAnotherIsland() {
  // ...island code here
}

Grouping islands in a single file reduces the number of request a site has to make and can even make some sites a little faster! Note, that whilst it’s tempting to put all islands in the same file that pattern might have the opposite effect. A good rule of thumb is that grouping islands that are used together on the same page gives you the most performant result.

Thanks to Reed von Redwitz for the contribution.

Fresh linting rules

The best way to give feedback is right in the editor. We started to keep track of common accidental mistakes like naming the handler export handlers instead of handler and integrated that into our linter. You’ll get those squiggly lines right in your editor whenever we detect that something is wrong.

screenshot of lint

A common source for confusion were the often cryptic error messages Fresh presented to the user when something was misconfigured or went outright wrong. We went through the most common errors and rewrote the error messages to be much more human readable. Whilst we think we covered a large area already, there are probably more cases where we can do a better job. So if you run into any error message that’s confusing to you, reach out to us!

The new linting rules work out of the box for new Fresh projects. To use them with existing projects just put this in your deno.json file:

{
  "lint": {
    "rules": {
      "tags": ["fresh", "recommended"]
    }
  }
}

Support for Deno.serve

With the recent Deno 1.35.0 release, the Deno.serve API was marked stable. We followed suit in Fresh and with version 1.3 we’ll use Deno.serve when it’s available. This new API is not just faster, but also a lot simpler than the previous serve API from std/http.

Thanks to Lino Le Van for the contribution.

More quality of life improvements

We shipped a bunch of minor improvements like being able to access route parameters from every handler or middleware. BigInt values can now be passed to islands as props. The starter template looks a lot better now, and much more!

Screenshot