astro-react-integrator
Integrate React components in Astro with optimal client directives (load/idle/visible/media/only)
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: astro-react-integrator
description: Integrate React components in Astro with optimal client directives (load/idle/visible/media/only)
activation:
keywords:
- react component
- client directive
- hydration
- client:load
- client:idle
- client:visible
triggers:
- "client:"
auto_load_project: cv-astro
---
# Astro React Integrator
## What This Is
Integrate React components in Astro with **optimal client directives** for maximum performance:
- Choose correct directive (load/idle/visible/media/only)
- TypeScript Props interface
- i18n integration with `useTranslation`
- Performance optimization (lazy loading)
**For**: CV_Astro project (Astro 5.5.2 + React 18)
---
## When to Activate
Auto-activates when keywords detected:
- "react component in astro"
- "client:load", "client:idle", "client:visible"
- "hydration"
- "interactive component"
---
## Client Directives Decision Tree
```
┌─────────────────────────────────────────────────┐
│ Is interaction CRITICAL for page load? │
│ (e.g., ContactForm, Navigation) │
└────────────┬────────────────────────────────────┘
│
YES │ NO
▼ ▼
client:load Is below the fold?
(Hydrate now) │
YES │ NO
▼ ▼
client:visible Is interaction
(When in view) immediate?
│
YES │ NO
▼ ▼
client:idle client:media
(When idle) (Media query)
```
---
## Client Directives Guide
### 1. **`client:load`** - Critical Interactivity
**When**: User needs it immediately on page load
**Use Cases**:
- Contact forms
- Navigation menus (mobile)
- Critical user inputs
- Authentication forms
**Example**:
```astro
---
// src/pages/contact.astro
import ContactForm from '../components/ContactForm.tsx';
---
<ContactForm client:load locale={Astro.currentLocale || 'en'} />
```
**Trade-off**: ⚠️ Increases initial JS bundle, blocks rendering
---
### 2. **`client:idle`** - Defer Non-Critical
**When**: Interaction not needed immediately, can wait for browser idle
**Use Cases**:
- Animations
- Scroll effects
- Non-critical modals
- Tooltips
- Analytics
**Example**:
```astro
---
import ScrollAnimation from '../components/ScrollAnimation.tsx';
---
<ScrollAnimation client:idle />
```
**Trade-off**: ✅ Best for non-critical features (loads when CPU idle)
---
### 3. **`client:visible`** - Lazy Load Below Fold
**When**: Component below the fold, only hydrate when scrolled into view
**Use Cases**:
- Newsletter signup (footer)
- Image galleries (below fold)
- Comment sections
- Related content
**Example**:
```astro
---
import NewsletterSignup from '../components/NewsletterSignup.tsx';
---
<NewsletterSignup client:visible />
```
**Trade-off**: ✅ Excellent for below-fold content (IntersectionObserver)
---
### 4. **`client:media`** - Responsive Hydration
**When**: Only needed on specific viewport sizes
**Use Cases**:
- Mobile-only navigation
- Desktop-only sidebars
- Responsive features
**Example**:
```astro
---
import MobileMenu from '../components/MobileMenu.tsx';
---
<MobileMenu client:media="(max-width: 768px)" />
```
**Trade-off**: ✅ Only loads JS when media query matches
---
### 5. **`client:only`** - Skip SSR
**When**: Component has SSR issues (window, document dependencies)
**Use Cases**:
- Third-party widgets
- Browser-only libraries
- Components with `window` dependencies
**Example**:
```astro
---
import BrowserOnlyWidget from '../components/BrowserOnlyWidget.tsx';
---
<BrowserOnlyWidget client:only="react" />
```
**Trade-off**: ⚠️ No SSR, flash of unstyled content possible
---
## React Component Patterns
### 1. **Simple React Component** (No i18n)
```tsx
// src/components/Counter.tsx
import React, { useState } from 'react';
import type { FC } from 'react';
interface CounterProps {
initialCount?: number;
}
const Counter: FC<CounterProps> = ({ initialCount = 0 }) => {
const [count, setCount] = useState(initialCount);
return (
<div className="counter">
<p className="count">{count}</p>
<button onClick={() => setCount(count + 1)} className="btn">
Increment
</button>
</div>
);
};
export default Counter;
```
**Usage in Astro**:
```astro
---
import Counter from '../components/Counter.tsx';
---
<!-- Non-critical, load when idle -->
<Counter client:idle initialCount={10} />
```
---
### 2. **React Component with i18n**
```tsx
// src/components/ContactForm.tsx
import React, { useState } from 'react';
import type { FC, FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
interface ContactFormProps {
locale: string;
}
const ContactForm: FC<ContactFormProps> = ({ locale }) => {
const { t } = useTranslation(['common', 'contact']);
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setStatus('sending');
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
setStatus('success');
setFormData({ name: '', email: '', message: '' });
} else {
setStatus('error');
}
} catch (error) {
setStatus('error');
}
};
return (
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">{t('common:labels.name')}</label>
<input
type="text"
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t('contact:form.namePlaceholder')}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">{t('common:labels.email')}</label>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder={t('contact:form.emailPlaceholder')}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">{t('common:labels.message')}</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder={t('contact:form.messagePlaceholder')}
rows={5}
required
/>
</div>
<button type="submit" disabled={status === 'sending'} className="submit-btn">
{status === 'sending' ? t('contact:form.sending') : t('common:actions.contactMe')}
</button>
{status === 'success' && (
<p className="success-message">{t('contact:form.success')}</p>
)}
{status === 'error' && (
<p className="error-message">{t('contact:form.error')}</p>
)}
</form>
);
};
export default ContactForm;
```
**Usage in Astro**:
```astro
---
import ContactForm from '../components/ContactForm.tsx';
---
<!-- Critical form, load immediately -->
<ContactForm client:load locale={Astro.currentLocale || 'en'} />
```
---
### 3. **React Component with State Management**
```tsx
// src/components/LanguageSwitcher.tsx
import React from 'react';
import type { FC } from 'react';
import { useTranslation } from 'react-i18next';
interface LanguageSwitcherProps {
currentLocale: string;
}
const LanguageSwitcher: FC<LanguageSwitcherProps> = ({ currentLocale }) => {
const { i18n } = useTranslation();
const languages = [
{ code: 'en', label: 'English', flag: '🇬🇧' },
{ code: 'es', label: 'Español', flag: '🇪🇸' },
{ code: 'fr', label: 'Français', flag: '🇫🇷' }
];
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode);
// Navigate to new locale URL
const currentPath = window.location.pathname;
const pathWithoutLocale = currentPath.replace(/^\/(es|fr)/, '');
const newPath = langCode === 'en'
? pathWithoutLocale || '/'
: `/${langCode}${pathWithoutLocale}`;
window.location.href = newPath;
};
return (
<div className="language-switcher">
{languages.map(lang => (
<button
key={lang.code}
onClick={() => handleLanguageChange(lang.code)}
className={`lang-btn ${currentLocale === lang.code ? 'active' : ''}`}
aria-label={`Switch to ${lang.label}`}
>
<span className="flag">{lang.flag}</span>
<span className="label">{lang.label}</span>
</button>
))}
</div>
);
};
export default LanguageSwitcher;
```
**Usage in Astro**:
```astro
---
import LanguageSwitcher from '../components/LanguageSwitcher.tsx';
---
<!-- Critical navigation, load immediately -->
<LanguageSwitcher client:load currentLocale={Astro.currentLocale || 'en'} />
```
---
## Integration Workflow
### Step 1: Analyze Interactivity Need
**Questions**:
1. Is interaction critical for page load? → `client:load`
2. Is component below the fold? → `client:visible`
3. Can interaction wait? → `client:idle`
4. Only for specific viewport? → `client:media`
5. Has SSR issues? → `client:only`
### Step 2: Create React Component
**Pattern**: `src/components/{ComponentName}.tsx`
**TypeScript interface**:
```tsx
interface ComponentNameProps {
requiredProp: string;
optionalProp?: number;
locale?: string; // If using i18n
}
```
### Step 3: Add i18n (if user-facing)
**Import**:
```tsx
import { useTranslation } from 'react-i18next';
```
**Usage**:
```tsx
const { t } = useTranslation(['common', 'namespace']);
```
**Translations**:
```tsx
<button>{t('common:actions.submit')}</button>
```
### Step 4: Integrate in Astro Page
```astro
---
import ComponentName from '../components/ComponentName.tsx';
---
<ComponentName
client:idle // Choose appropriate directive
requiredProp="value"
locale={Astro.currentLocale || 'en'}
/>
```
---
## Performance Optimization
### Measure Impact
**Before optimization**:
```bash
npm run build
# Check bundle size in dist/_astro/
```
**After choosing correct directive**:
- `client:load` → Largest bundle
- `client:idle` → Medium bundle (deferred)
- `client:visible` → Smallest bundle (lazy)
### Best Practices
1. **Default to `client:idle`** unless:
- Critical for UX → `client:load`
- Below fold → `client:visible`
- Viewport-specific → `client:media`
2. **Minimize `client:load` usage**:
- Only for forms, navigation, auth
- Everything else → `client:idle` or `client:visible`
3. **Split large components**:
```astro
<!-- ❌ Bad: Load everything at once -->
<LargeComponent client:load />
<!-- ✅ Good: Split and lazy load -->
<CriticalPart client:load />
<NonCriticalPart client:idle />
```
4. **Use `client:visible` for below-fold**:
```astro
<Header client:load /> <!-- Above fold -->
<Newsletter client:visible /> <!-- Below fold -->
<Footer client:visible /> <!-- Below fold -->
```
---
## Common Patterns
### Pattern 1: Form (Critical)
```astro
---
import ContactForm from '../components/ContactForm.tsx';
---
<ContactForm
client:load // Critical: User needs immediately
locale={Astro.currentLocale || 'en'}
/>
```
---
### Pattern 2: Animation (Non-Critical)
```astro
---
import ScrollReveal from '../components/ScrollReveal.tsx';
---
<ScrollReveal
client:idle // Non-critical: Can wait
/>
```
---
### Pattern 3: Below Fold (Lazy)
```astro
---
import Newsletter from '../components/Newsletter.tsx';
---
<Newsletter
client:visible // Below fold: Load when scrolled
locale={Astro.currentLocale || 'en'}
/>
```
---
### Pattern 4: Mobile-Only (Responsive)
```astro
---
import MobileMenu from '../components/MobileMenu.tsx';
---
<MobileMenu
client:media="(max-width: 768px)" // Mobile only
/>
```
---
## Validation
After integration, check:
- [ ] Correct directive chosen (load/idle/visible/media/only)
- [ ] TypeScript Props interface defined
- [ ] i18n integrated (if user-facing text)
- [ ] Locale prop passed (if i18n)
- [ ] Component exported as default
- [ ] No console errors in browser
- [ ] Performance: Check bundle size
- [ ] Accessibility: Test keyboard navigation
---
## Quick Reference
| Directive | When | Example |
|-----------|------|---------|
| `client:load` | Critical, immediate | Contact form, navigation |
| `client:idle` | Non-critical, can wait | Animations, tooltips |
| `client:visible` | Below fold, lazy | Newsletter, footer widgets |
| `client:media` | Viewport-specific | Mobile menu, responsive |
| `client:only` | SSR issues | Browser-only libraries |
**Default recommendation**: Start with `client:idle`, optimize to `client:visible` or `client:load` as needed.