Hello and welcome back!
In our last lesson, we explored Astro's Islands Architecture and learned how to use client:* directives to control precisely when our React components become interactive. You now have the power to optimize performance by loading JavaScript only when it's needed.
Today, we'll bridge the gap between Astro's static, server-rendered environment and your interactive React islands. The learning outcome for this lesson is to pass props from Astro components to React components. This is the fundamental mechanism for making your components dynamic and data-driven. For your accounting website, this skill will be essential for populating service descriptions, author names on blog posts, and testimonial content from a central source.
Let's get started.
1. The Basics of Passing Props
Given your experience with React, the syntax for passing props in Astro will feel immediately familiar. It's just like passing props from a parent to a child component in JSX.
In an Astro file (.astro), you can define variables in the frontmatter script (the top --- block). These variables can then be passed to your React components as attributes.
Let's imagine you have a simple React component Greeting.tsx that accepts a message prop:
src/components/Greeting.tsx
import React from 'react';
interface GreetingProps {
message: string;
}
export default function Greeting({ message }: GreetingProps) {
return (
<div style={{ padding: '1rem', border: '1px solid #ccc' }}>
<h2>A Message for You:</h2>
<p>{message}</p>
</div>
);
}
In your Astro page, you can import and use this component, passing a string from the Astro frontmatter:
src/pages/props-test.astro
---
import Greeting from '../components/Greeting.tsx';
const welcomeMessage = "Welcome to the future of accounting websites!";
---
<Greeting message={welcomeMessage} />
Notice something? We didn't add a client:* directive. By default, Astro will render this React component to static HTML on the server. The output will be a div with the message inside. No client-side JavaScript is shipped.
If the component needs to be interactive (e.g., have a button with an onClick handler), you would add a directive:
<Greeting message={welcomeMessage} client:load />
The key takeaway is that the prop-passing mechanism works identically for both static and interactive components. The difference lies in what happens next. For interactive components, Astro has to do something special with those props.
2. The Key Concept: Serialization
When you pass props to an interactive component (one with a client:* directive), Astro needs to send that data from the server to the client's browser. The server and browser are separate environments that can't share variables directly.
To solve this, Astro serializes the props. It converts your data into a string format (specifically, JSON) and embeds it into the HTML sent to the browser. When the component "hydrates" in the browser, Astro's client-side code parses this JSON string and passes the reconstructed data to your React component.
This process has an important implication: you can only pass prop values that are serializable to JSON.
To understand this crucial limitation, let's turn to the official documentation.
Front-end frameworks - Astro Docs
The official Astro documentation provides a clear explanation of how to pass props and, crucially, what limitations exist. Pay close attention to the list of supported data types.
Read the section titled 'Passing props to framework components'. Focus on the list of supported prop types and the explanation of why functions cannot be passed to interactive components.
As you just read, supported types include primitives (string, number, boolean), as well as Array, plain Object, Date, Map, Set, and others.
The most important takeaway is what is not supported: functions. You cannot pass a function from an Astro component to an interactive React component. This is a fundamental constraint of the Islands Architecture. The function exists on the server during the build, but it can't be "sent" to the browser to be executed later. All event handlers (onClick, onSubmit, etc.) must be defined inside your React component.
Test your understanding!
You are building a page for the accounting website that lists various services. You have the following data and a function in your Astro page's frontmatter:
const services = [
{ id: 'tax', name: 'Tax Planning' },
{ id: 'audit', name: 'Auditing Services' }
];
function logServiceName(name) {
console.log(`Service selected: ${name}`);
}
You want to pass this data to an interactive React component: <ServiceList client:visible />.
Which of the following props can be successfully passed and used in the hydrated component?
services={services}onItemClick={logServiceName}
Show answer
services={services}will work. Theservicesvariable is an array of plain JavaScript objects, which is a JSON-serializable data type. Astro can convert this array to a JSON string and send it to the client.onItemClick={logServiceName}will NOT work. ThelogServiceNamevariable is a function. Functions are not serializable and cannot be passed from the server-side Astro context to a client-side interactive component. The logic for what happens when an item is clicked must be defined within theServiceListReact component itself.
3. Practical Exercise: Building a Dynamic Profile Card
Let's build a component that reflects a realistic use case for your project: a profile card for an accountant. This will involve passing an object and an array.
-
Create the React Component
Create a new file at
src/components/ProfileCard.tsx. This component will take apersonobject and aclient:visibledirective, so it will only become interactive when it scrolls into view.src/components/ProfileCard.tsximport React, { useState } from 'react'; // Define the shape of the props we expect interface ProfileCardProps { person: { name: string; title: string; avatarUrl: string; specialties: string[]; }; } export default function ProfileCard({ person }: ProfileCardProps) { const [isExpanded, setIsExpanded] = useState(false); const toggleExpansion = () => { setIsExpanded(!isExpanded); }; return ( <div style={{ border: '1px solid #eee', borderRadius: '8px', padding: '1.5rem', maxWidth: '350px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', fontFamily: 'sans-serif' }}> <img src={person.avatarUrl} alt={`Avatar of ${person.name}`} style={{ width: '80px', height: '80px', borderRadius: '50%' }} /> <h3 style={{ marginTop: '1rem', marginBottom: '0.25rem' }}>{person.name}</h3> <p style={{ color: '#666', marginTop: 0 }}>{person.title}</p> <button onClick={toggleExpansion} style={{ marginTop: '1rem' }}> {isExpanded ? 'Hide Specialties' : 'Show Specialties'} </button> {isExpanded && ( <div style={{ marginTop: '1rem' }}> <strong>Specialties:</strong> <ul style={{ paddingLeft: '20px', marginTop: '0.5rem' }}> {person.specialties.map((specialty, index) => ( <li key={index}>{specialty}</li> ))} </ul> </div> )} </div> ); }Notice that the
toggleExpansionevent handler is defined inside the React component. -
Use the Component in an Astro Page
Now, create a new page
src/pages/team.astroto consume this component. We'll define the data directly in the frontmatter. In a real project, this data would come from your MDX content collections.src/pages/team.astro--- import BaseLayout from '../layouts/BaseLayout.astro'; import ProfileCard from '../components/ProfileCard.tsx'; // This data would eventually come from a content collection. const accountantProfile = { name: 'John Carter', title: 'Senior Partner, CPA', avatarUrl: 'https://i.pravatar.cc/150?u=johncarter', // A placeholder image service specialties: ['Corporate Tax', 'International Accounting', 'Audit & Assurance'] }; --- <BaseLayout title="Our Team"> <div style={{ maxWidth: '60ch', margin: '2rem auto' }}> <h1>Meet Our Lead Accountant</h1> <p>Our team is led by experienced professionals dedicated to your success.</p> <div style={{ marginTop: '2rem' }}> <ProfileCard person={accountantProfile} client:visible /> </div> </div> </BaseLayout> -
Run and Verify
Start your dev server (
npm run dev) and navigate to/team. You should see the profile card. When you click the "Show Specialties" button, the component becomes interactive and reveals the list. The object and array we defined in Astro's frontmatter were successfully serialized and passed to the React component as props.
4. Passing Children and Named Slots
Finally, it's worth knowing that you can pass content into a React component from an Astro component using the standard children prop and a powerful feature called "named slots."
-
children: Anything you place between the opening and closing tags of your component in Astro is passed to React'sprops.children.<!-- In team.astro --> <ProfileCard person={accountantProfile} client:visible> <p style={{fontSize: '0.9em', fontStyle: 'italic'}}>John has over 20 years of experience.</p> </ProfileCard>This paragraph would be available inside
ProfileCard.tsxviaprops.children. -
Named Slots: You can structure content more precisely using the
slotattribute.<!-- In team.astro --> <MyReactComponent> <h2 slot="header-title">My Title</h2> <p>This is the default content (children).</p> <div slot="footer-content">Some footer info</div> </MyReactComponent>Inside your React component, these slots are converted to props. Astro handily converts
kebab-caseslot names tocamelCasefor you.// Inside MyReactComponent.tsx export default function MyReactComponent(props) { return ( <div> <header>{props.headerTitle}</header> <main>{props.children}</main> <footer>{props.footerContent}</footer> </div> ); }
This is a very powerful pattern for creating flexible, reusable layout components.
Conclusion
You have now mastered a critical link in the Astro-React toolchain. You can create interactive islands with React and fuel them with data from Astro's server-side environment.
Key Takeaways:
- Passing props in Astro uses the same familiar JSX attribute syntax as React.
- Props passed to interactive components (
client:*) must be serializable into JSON. - Primitives, plain objects, and arrays work perfectly.
- Functions cannot be passed as props to hydrated components; event logic must live inside the component itself.
- You can pass content via
props.childrenand structure it with named slots.
Up Next:
Now that you can get initial data into your React components, the next step is to make those components fully dynamic by managing their own internal state. In the next lesson, we will explore how to use React hooks like useState and useEffect effectively within your Astro islands.