Skip to content

EreoJS vs Astro

EreoJS and Astro both embrace islands architecture, but with different approaches. Astro is content-focused with multi-framework support, while EreoJS is React-focused with full-stack capabilities.

Overview

AspectEreoJSAstro
FocusFull-stack appsContent sites
FrameworkReact onlyMulti-framework
IslandsReact islandsAny framework
Data PatternsLoaders/ActionsContent collections, fetch
RenderingSSR/SSG/StreamingSSG-first, SSR optional
RuntimeBunNode.js

Philosophy

Astro:

  • Content-first design
  • Ship zero JS by default
  • Multi-framework islands (React, Vue, Svelte, etc.)
  • Static-first with SSR as opt-in
  • .astro component format

EreoJS:

  • Application-first design
  • React throughout
  • Progressive enhancement
  • SSR-first with SSG as option
  • Standard React/TSX components

Component Syntax

Astro:

astro
---
// Component script (server)
const posts = await getPosts()
---

<!-- Component template -->
<div>
  {posts.map(post => (
    <article>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </article>
  ))}
</div>

<style>
  article { margin: 1rem; }
</style>

EreoJS:

tsx
import { createLoader } from '@ereo/data'

export const loader = createLoader(async () => {
  const posts = await getPosts()
  return { posts }
})

export default function Posts({ loaderData }) {
  return (
    <div>
      {loaderData.posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Key differences:

  • Astro has its own .astro format
  • EreoJS uses standard React/TSX
  • Both separate data from rendering

Islands Architecture

Astro:

astro
---
import Counter from '../components/Counter.jsx'
---

<h1>Static content</h1>

<!-- Hydration directives -->
<Counter client:load />
<Counter client:idle />
<Counter client:visible />
<Counter client:media="(max-width: 768px)" />

EreoJS:

For simple cases, use 'use client' (hydrates on load):

tsx
// app/components/Counter.tsx
'use client';
// ... component code
tsx
import { Counter } from '~/components/Counter';

export default function Page() {
  return (
    <>
      <h1>Static content</h1>
      <Counter />
    </>
  )
}

For explicit hydration strategies (like Astro's client:*), use data-island attributes:

tsx
<Counter data-island="Counter" data-hydrate="load" />
<Counter data-island="Counter" data-hydrate="idle" />
<Counter data-island="Counter" data-hydrate="visible" />
<Counter
  data-island="Counter"
  data-hydrate="media"
  data-media="(max-width: 768px)"
/>

Similar hydration strategies:

  • client:load / data-hydrate="load" - Immediate
  • client:idle / data-hydrate="idle" - When idle
  • client:visible / data-hydrate="visible" - When visible
  • client:media / data-hydrate="media" - Media query

Data Fetching

Astro:

astro
---
// Direct fetch in frontmatter
const posts = await fetch('https://api.example.com/posts')
  .then(r => r.json())
---

<ul>
  {posts.map(post => <li>{post.title}</li>)}
</ul>

EreoJS:

tsx
export const loader = createLoader(async () => {
  const posts = await fetch('https://api.example.com/posts')
    .then(r => r.json())
  return { posts }
})

export default function Posts({ loaderData }) {
  return (
    <ul>
      {loaderData.posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

Content Collections

Astro:

ts
// src/content/config.ts
import { defineCollection, z } from 'astro:content'

const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    date: z.date(),
  })
})

export const collections = { blog }
astro
---
import { getCollection } from 'astro:content'

const posts = await getCollection('blog')
---

EreoJS:

EreoJS doesn't have built-in content collections. Use file-based content:

tsx
// lib/content.ts
import { glob } from 'bun'
import matter from 'gray-matter'

export async function getPosts() {
  const files = glob.sync('content/blog/*.md')
  return files.map(file => {
    const content = Bun.file(file).text()
    const { data, content: body } = matter(content)
    return { ...data, body }
  })
}

Form Handling

Astro:

astro
---
// No built-in form handling
// Use API endpoints or client-side
---

<form action="/api/contact" method="post">
  <input name="email" type="email" />
  <button type="submit">Subscribe</button>
</form>
ts
// src/pages/api/contact.ts
export async function POST({ request }) {
  const data = await request.formData()
  // Handle form...
  return new Response(JSON.stringify({ ok: true }))
}

EreoJS:

tsx
import { createAction, redirect } from '@ereo/data'
import { Form, useActionData } from '@ereo/client'

export const action = createAction(async ({ request }) => {
  const data = await request.formData()
  const email = data.get('email')

  if (!isValidEmail(email)) {
    return { error: 'Invalid email' }
  }

  await subscribe(email)
  return redirect('/thank-you')
})

export default function Subscribe() {
  const actionData = useActionData()

  return (
    <Form method="post">
      <input name="email" type="email" />
      {actionData?.error && <p>{actionData.error}</p>}
      <button type="submit">Subscribe</button>
    </Form>
  )
}

EreoJS has first-class form support with progressive enhancement.

Routing

Astro:

src/pages/
├── index.astro        # /
├── about.astro        # /about
├── posts/
│   ├── index.astro    # /posts
│   └── [slug].astro   # /posts/:slug
└── api/
    └── users.ts       # /api/users

EreoJS:

routes/
├── index.tsx          # /
├── about.tsx          # /about
├── posts/
│   ├── index.tsx      # /posts
│   └── [slug].tsx     # /posts/:slug
└── api/
    └── users.ts       # /api/users

Very similar structures.

SSR vs SSG

Astro (SSG-first):

astro
---
// Static by default
export const prerender = true // Explicit SSG (optional)

// For SSR
export const prerender = false
---

EreoJS (SSR-first):

tsx
export const config = {
  render: 'ssr'  // Default
  // Or
  render: 'ssg'  // Static generation
}

Multi-Framework Support

Astro:

astro
---
import ReactCounter from '../components/ReactCounter.jsx'
import VueCounter from '../components/VueCounter.vue'
import SvelteCounter from '../components/SvelteCounter.svelte'
---

<ReactCounter client:load />
<VueCounter client:load />
<SvelteCounter client:load />

EreoJS:

EreoJS is React-only. This is by design for simplicity and consistency.

Middleware

Astro:

ts
// src/middleware.ts
export function onRequest({ request }, next) {
  console.log(request.url)
  return next()
}

EreoJS:

ts
// routes/_middleware.ts
export const middleware = async (request, context, next) => {
  console.log(request.url)
  return next()
}

Similar patterns, but EreoJS supports route-level middleware files.

Build Output

Astro:

  • Static HTML files by default
  • Partial hydration chunks
  • Adapter-based SSR output

EreoJS:

  • Server bundle
  • Client bundles per island
  • Static assets

Performance Comparison

MetricEreoJSAstro
Build SpeedFaster (Bun)Fast (esbuild)
JS ShippedIslands onlyIslands only
First PaintSimilarSimilar
Time to InteractiveSimilarSimilar
SSR Response TimeFaster (Bun)Good

Both ship minimal JavaScript through islands.

Use Cases

Choose Astro for:

  • Documentation sites
  • Blogs
  • Marketing sites
  • Content-heavy sites
  • Multi-framework teams

Choose EreoJS for:

  • Full-stack applications
  • Forms and mutations
  • React-only teams
  • Complex interactivity
  • Real-time features

Migration Considerations

Astro to EreoJS

  1. Convert .astro files to React components
  2. Move data fetching to loaders
  3. Convert client:* directives to 'use client' (simple) or data-island attributes (for hydration control)
  4. Add actions for form handling
  5. Update build configuration

EreoJS to Astro

  1. Convert React components to .astro (for static) or keep React (for islands)
  2. Move loaders to frontmatter or API routes
  3. Convert 'use client' / data-island to client:* directives
  4. Use API routes for form handling

Summary

Both frameworks embrace the islands architecture concept but serve different purposes:

  • Astro is ideal for content-focused sites where you want to use multiple frameworks and ship minimal JavaScript
  • EreoJS is ideal for full-stack React applications where you need forms, mutations, and complex server-side logic

The choice depends on your use case: content site vs. full-stack application.

Released under the MIT License.