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
- Adding routes and/or middlewares from plugins
- 500 error template fallback
- Error Boundaries
- Export multiple islands in the same file
- Fresh linting rules
- Support for Deno.serve
- More quality of life improvements
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.
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!