Notes on rewriting JSX as Astro
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 This has since been fixed.class:list
directive triggers TypeScript errors.
Also, I was getting a weird lint error on a My bad, I couldn’t find the screenshot, and I see all <meta>
tag.<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:
- Each
.astro
file must export exactly one component- You can’t create component groups or collections in a single file
- You can’t export constants from
.astro
modules, which sometimes breaks data colocation patterns - You can’t access
Astro.site
outside of.astro
files- Probably the global variable
Astro
doesn’t exist outside of.astro
files
- Probably the global variable
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:

Restarting the development server and refreshing the web browser has been enough to get around this.
Syntax differences
<slot />
instead of{children}
- Slots can have “names”
- Similar to passing component sub-trees via props in JSX
- Slots can have “names”
class="foo"
in place ofclassName="bar"
- HTML-style comments:
<!-- baz -->
; instead of JSX-style:{/* qux */}
- No
key
prop needed when using.map()
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
- You can’t manipulate components inside JavaScript fences
- Astro delimits a component’s JS code within two “fences”:
---
- Similar to a Svelte component’s
<script></script>
block
- Astro delimits a component’s JS code within two “fences”:
- You can’t import and render Astro components within JSX components
getStaticPaths
forgot how to closure…- What this means is you can’t access a symbol defined outside the scope of this function
- It’s probably being sliced out of the component file without regard for the surrounding scope
- Dynamic route files like
[tag].astro
can’t be reused as regular components - The lack of component symbols loses the “find references” functionality you’d have in a React codebase
- Wherein we can just
cmd + click
the component symbol
- Wherein we can just
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!