AI-generated images of React and Astro's logo together, arranged in a grid

Notes on rewriting JSX as Astro

Carlos Neves

After spending time building with Astro, I’ve collected some observations about the developer experience. Overall, Astro is a solid framework that gets the job done, though it has some quirks worth noting if you’re coming from JSX-land.

Developer experience differences

VS Code integration

I feel like the tooling experience has space for some improvements. The command to select the nearest statement doesn’t work. Automatic imports and autocomplete suggestions can be inconsistent.

Using the class:list directive triggers TypeScript errors. This has since been fixed.

Also, I was getting a weird lint error on a <meta> tag. My bad, I couldn’t find the screenshot, and I see all <meta> tags are good now.

Props and component architecture

Astro’s props contract feels a bit awkward coming from JSX. Instead of receiving props as a function argument, you access them through a global Astro.props variable. E.g.: const { foo } = Astro.props. This makes components feel a bit less like functions. I’ve since learned Svelte (runes) has a similar syntactical construct: const { bar } = $props().

The component model has some limitations:

Styling

Astro has a styling system similar to Svelte, in that you can just open a <style> tag within the component, and that style will remain scoped by default. Looking back on the components I wrote, I can probably cut down most of the CSS files I have. Most components can have all of their HTML/CSS/JS code fit within a single screen.

Icons

I probably didn’t search deeply enough, but bringing in icons wasn’t as easy as running npm install astro-icons and then importing the icon components: import { MdPlay } from "astro-icons/md".

Layout Bug

Whenever playing with Astro, sometimes it will inject some framework HTML within a <pre> element, causing a layout issue:

A screenshot of an Astro layout bug; framework markup is inserted within a pre-formatted white-space element

Restarting the development server and refreshing the web browser has been enough to get around this.

Syntax differences

VS Code’s autocomplete sometimes fights you. When writing a prop and its value, often you want to write prop={value}, but as soon as you type equals =, double quotes "" are immediately inserted in the file before you can type {.

Markdown and MDX pain points

Astro encourages using Markdown and MDX, but the integration has gaps:

Heading extraction

When you write <h2>Foo</h2> in MDX instead of ## Foo, it doesn’t get extracted as a title. Also, you can’t do ## <MarkedUpText /> and have the headings property just work… In the end, I had to resort to jsdom to properly parse all headings.

Component customization

You can’t easily replace default markdown elements with custom components at the project level. For example, to have `foo` become <MyCode>foo</MyCode>, or have > bar become <MyBlockQuote>bar</MyBlockQuote>. As far as I searched, the working way is to set this custom mapping inside each MDX file you have. It would be awesome if we could set this mapping at the project level, maybe in Astro’s config file. Ultimately, I had to resort to components and drop the nice markdown syntax.

(Astro) Islands are a cool concept

Client islands, specifically, enable you to bundle and ship framework-agnostic dynamic components to the client. At the moment, Astro offers support for React, Vue, Preact, Svelte, and Solid. The surrounding page markup is static, and only that dynamic component will follow the flow of loading the framework runtime and rendering on the client-side.

Here’s an example dynamic island written in Svelte:

Architecture limitations

Whitespace and formatting struggles

Whitespace handling is a bit frustrating. Astro doesn’t trim whitespace by default, and sometimes you simply can’t get the spacing right. Prettier and Astro often disagree about formatting. Sometimes Prettier will indent etc., and Astro will render unwanted white-space into our HTML.

Script optimization

Script inlining is all or nothing, so you can’t have Astro selectively minify inlined scripts.

Conclusion

Astro is a solid framework for getting things done. The tooling will get there eventually. Some patterns may feel less elegant than what you might find elsewhere. Anyhow, Astro manages to set in place a pleasant DSL to more neatly segregate build-time code from client code. With that being said, this very blog is now written in Astro, and I’ll likely use it again on other projects. Who knows? Maybe someday I rewrite the whole lot in something else entirely.

Happy building!