Type something to search...
How to use React components in Astro?

How to use React components in Astro?

To use React components in Astro, install the official @astrojs/react integration with npx astro add react, then import your .jsx or .tsx component into any .astro file and use it like an HTML tag. By default Astro renders the React component to static HTML on the server and sends no JavaScript. To make it interactive, add a client directive such as client:load or client:visible, and Astro will ship React and your component's code to the browser and hydrate it. Props work as normal, children and named slots are passed in as React props, and each hydrated component runs as its own independent island.

React is the most common framework people bring into Astro, either to reuse an existing component library or because the team already knows it. The integration is mature, but a few things work differently from a React single-page app. In this article you'll learn how to install and configure React, how server rendering and hydration fit together, how to pass props, children and slots, what changed in the React integration for Astro 7, and the limitations to plan around.

Installing the React Integration

The quickest way is the astro add command, which installs the packages and edits your config for you:

npx astro add react

It makes three changes, and asks you to confirm each one:

  • Dependencies: installs @astrojs/react, react, react-dom, @types/react and @types/react-dom.
  • Astro config: imports the integration and adds it to integrations.
  • TypeScript config: sets jsx and jsxImportSource so .tsx files type-check correctly.

After it runs, your config looks like this:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react()],
});

And tsconfig.json gains a compilerOptions block:

{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"],
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}

If you prefer to install manually, run npm install @astrojs/react react react-dom @types/react @types/react-dom and make the same two config edits yourself. At the time of writing the current versions are @astrojs/react 7.1.1 and React 19.

Writing and Rendering a React Component

React components in Astro are ordinary React components. Nothing Astro-specific goes inside them:

// src/components/Counter.tsx
import { useState } from 'react';

interface Props {
  label: string;
  start?: number;
}

export default function Counter({ label, start = 0 }: Props) {
  const [count, setCount] = useState(start);

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      {label}: {count}
    </button>
  );
}

Import it in the frontmatter of an Astro page or component and use it in the template:

---
// src/pages/index.astro
import Counter from '../components/Counter';
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React in Astro</title>
  </head>
  <body>
    <Counter label="Clicks" start={5} />
  </body>
</html>

This renders a button reading "Clicks: 5", but clicking it does nothing. Astro ran the component on the server, turned the result into HTML and threw the JavaScript away. That's the default for every framework component, and it's useful: you can build layouts, cards and content blocks in React without shipping React to the browser.

Making the Component Interactive

Add a client directive to hydrate the component in the browser:

<Counter label="Clicks" start={5} client:load />

Now Astro still renders the HTML on the server, but it also wraps the button in an <astro-island> element and loads React and Counter's code so the button works. The main options are:

  • client:load: hydrate immediately on page load.
  • client:idle: hydrate once the browser is idle after loading.
  • client:visible: hydrate when the component scrolls into view.
  • client:media: hydrate when a CSS media query matches.
  • client:only="react": skip server rendering and render only in the browser.

Each is explained in detail in what client:load, client:idle and client:visible mean.

React itself is downloaded once per page no matter how many React islands you hydrate. It's still the largest of the common framework runtimes: in my test build the React renderer chunk was roughly 66 KB gzipped, compared with about 29 KB for Vue and 12 to 14 KB for Svelte. That's a good reason to hydrate only the components that truly need it.

Passing Props

Props from Astro reach the React component exactly as you'd expect, including during server rendering. For hydrated components, though, Astro has to send the props to the browser, so they must be serialisable. The supported types are plain objects, number, string, Array, Map, Set, RegExp, Date, BigInt, URL, Uint8Array, Uint16Array, Uint32Array and Infinity.

Astro restores these types properly on the client, not just as JSON strings. I tested this with a component that logs whether each prop arrived as the right type:

// src/components/PropTypes.tsx
import { useEffect } from 'react';

interface Props {
  published: Date;
  tags: Set<string>;
  stock: Map<string, number>;
  homepage: URL;
}

export default function PropTypes({ published, tags, stock, homepage }: Props) {
  useEffect(() => {
    console.log(
      published instanceof Date,
      tags instanceof Set,
      stock instanceof Map,
      homepage instanceof URL,
    );
  }, []);

  return (
    <p>
      {published.toISOString().slice(0, 10)} {[...tags].join(', ')} {stock.get('mug')} {homepage.hostname}
    </p>
  );
}
<PropTypes
  published={new Date('2026-10-24')}
  tags={new Set(['astro', 'react'])}
  stock={new Map([['mug', 12]])}
  homepage={new URL('https://sajjad.me/blog')}
  client:load
/>

The browser console printed:

true true true true

What you can't pass is a function. An onClick or onChange handler written in Astro frontmatter can't be sent to the browser, because there is no way to serialise executable code. Define event handlers inside the React component, or have the component import the logic it needs.

Passing Children and Named Slots

Content between a React component's tags in Astro arrives as children. Elements with a slot attribute become separate top-level props, and kebab-case slot names are converted to camelCase:

// src/components/Card.tsx
import type { ReactNode } from 'react';

interface Props {
  title?: ReactNode;
  footerLinks?: ReactNode;
  children?: ReactNode;
}

export default function Card({ title, footerLinks, children }: Props) {
  return (
    <article className="card">
      <header>{title}</header>
      <div className="card-body">{children}</div>
      <footer>{footerLinks}</footer>
    </article>
  );
}
---
// src/pages/index.astro
import Card from '../components/Card';
---
<Card>
  <h2 slot="title">Menu</h2>
  <p>Main content goes to children.</p>
  <ul slot="footer-links">
    <li><a href="/about">About</a></li>
  </ul>
</Card>

The built HTML puts each piece where you'd expect:

<article class="card"><header><h2>Menu</h2></header><div class="card-body"><p>Main content goes to children.</p></div><footer><ul><li><a href="/about">About</a></li></ul></footer></article>

This also lets you put Astro components inside React components. You can't import a .astro file into a .tsx file, but you can pass one in as a child or slot from an Astro page.

Children Are Strings, Not React Nodes

By default, children passed from Astro are rendered HTML, not React elements. A React component that inspects its children with React.Children.map() or expects several separate child elements will only see a single child. If a library depends on that, enable the integration's experimental option:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [
    react({
      experimentalReactChildren: true,
    }),
  ],
});

This converts children into real React virtual DOM nodes at some runtime cost. Leave it off unless you need it.

Render Props Won't Work

Because Astro templates are rendered on the server and have no client runtime, patterns that pass a function as a child, like <List>{(item) => <li>{item}</li>}</List>, don't work from an Astro file. Use named slots instead, or move the whole tree into a parent React component.

What Changed in @astrojs/react 7

The React integration released alongside Astro 7 made two changes worth knowing if you're upgrading:

  • Oxc instead of Babel: JSX and Fast Refresh are now compiled with Oxc, and the integration's babel option has been removed. If you relied on Babel plugins, such as babel-plugin-styled-components, move them to @rolldown/plugin-babel under vite.plugins in your Astro config.
  • Optional React Compiler: a new compiler option turns on automatic memoisation through the experimental Oxc React Compiler. It requires the oxc-transform-react package.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react({ compiler: true })],
});

The compiler skips server rendering, dependencies and .astro files, and only applies to client components and hooks.

Limitations to Plan Around

Each Island Is Its Own React Root

Two hydrated React components on the same page are two separate React applications. A context provider in one doesn't reach the other, and wrapping them both in a provider inside an Astro file won't help, because Astro renders the provider to static HTML. To share state between islands, use a small store library that lives outside React, as shown in sharing state between islands.

Browser-Only Code Breaks Server Rendering

React components run on the server during the build unless you use client:only. Code that reads window, document or localStorage while rendering will fail there. Move it into useEffect(), or use client:only="react" for components that can't render on the server at all.

Mixing JSX Frameworks Needs Extra Config

If you also add Preact or Solid, which use the same .jsx and .tsx extensions, you must tell each integration which files belong to it with include and exclude. Mixing React with Vue or Svelte needs no extra config because their file extensions differ. Both cases are covered in mixing multiple UI frameworks.


FAQ: React Components in Astro

No. Only add a client directive to components that need state, effects or event handlers in the browser. Others render to plain HTML and ship no JavaScript.

Yes, inside React components. Hooks like useState and useEffect work normally once the component is hydrated. You can't use hooks in .astro files, which are server templates.

Usually, yes. Libraries that render on the server work with any client directive. Libraries that touch browser globals during render may need client:only="react", and libraries that inspect children may need the experimentalReactChildren option.

No. Framework components can only import code from their own framework. Pass Astro components in as children or named slots from an .astro file instead.

Each hydrated component is a separate React root, so a provider only reaches components inside the same island. Use a shared store such as Nano Stores, or render the provider and its consumers inside one parent React component.

The React runtime is the same, but Astro only sends it when at least one component on the page is hydrated, and only hydrates the components you mark. Pages with little interactivity usually ship far less JavaScript as a result.


Conclusion

Using React in Astro takes one command and no changes to how you write components. Import a .tsx file into an Astro page and it renders to HTML on the server; add client:load, client:idle or client:visible and it becomes an interactive island. Props are serialised with support for rich types like Date and Map, children and named slots arrive as props, and the Astro 7 version of the integration swaps Babel for Oxc and adds an optional React Compiler.

The main shift from a React app is thinking in islands. Each hydrated component is independent, so share state with a store rather than context, keep functions out of props, and hydrate only what needs to be interactive. Do that and you get React's component model with much less JavaScript on every page.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper