Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 23

Astro to React: Passing Props

Recommended resources

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?

  1. services={services}
  2. onItemClick={logServiceName}
Show answer
  1. services={services} will work. The services variable 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.
  2. onItemClick={logServiceName} will NOT work. The logServiceName variable 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 the ServiceList React 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.

  1. Create the React Component

    Create a new file at src/components/ProfileCard.tsx. This component will take a person object and a client:visible directive, so it will only become interactive when it scrolls into view.

    src/components/ProfileCard.tsx

    import 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 toggleExpansion event handler is defined inside the React component.

  2. Use the Component in an Astro Page

    Now, create a new page src/pages/team.astro to 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>
    
  3. 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's props.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.tsx via props.children.

  • Named Slots: You can structure content more precisely using the slot attribute.

    <!-- 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-case slot names to camelCase for 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.children and 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.