> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# Create React App - Complete Setup Guide

Complete guide for setting up Meticulous with React applications built with Create React App (CRA).

---

## Overview

Create React App is the official way to create single-page React applications. This guide covers:

- **Recorder installation** in `public/index.html`
- **CI/CD configuration** with static asset upload
- **Authentication handling**
- **Common patterns** and troubleshooting

**Prerequisites**:
- React application created with Create React App
- Basic familiarity with [Meticulous concepts](/docs/onboarding-guide)

**Note**: Create React App is no longer actively maintained. For new projects, consider using [Vite](/docs/frameworks/react/vite) or Next.js.

---

## Quick Start

### Step 1: Install Recorder in public/index.html

Add the Meticulous recorder script to your `public/index.html` **before any other scripts**.

**File**: `public/index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Your app description" />
    <title>Your App Name</title>

    <!-- Meticulous recorder - MUST be first script -->
    <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
    <script
      data-project-id="YOUR_PROJECT_ID"
      src="https://snippet.meticulous.ai/v1/meticulous.js"
    ></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
```

**Important**: The recorder must load before React to capture all events.

---

### Step 2: Configure GitHub Actions Workflow

CRA builds to static files, so we use the `upload-assets` action.

**File**: `.github/workflows/meticulous.yml`

```yaml
name: Meticulous Tests

on:
  push:
    branches: [main]
  pull_request: {}
  workflow_dispatch: {}

permissions:
  actions: write
  contents: read
  issues: write
  pull-requests: write
  statuses: read

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build app
        run: npm run build
        env:
          NODE_ENV: production
          CI: false # Treats warnings as non-blocking

      - name: Upload and test
        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-directory: "build"
          rewrites: |
            [
              { "source": "/(.*)", "destination": "/index.html" }
            ]
```

**Note**: `CI: false` prevents build from failing on warnings. Remove if you want strict builds.

---

### Step 3: Add API Token Secret

1. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
2. Go to GitHub repo → Settings → Secrets and variables → Actions
3. Create secret named `METICULOUS_API_TOKEN` with your token

---

## How It Works

### upload-assets Action

The `upload-assets` action:
1. Uploads your built static files to Meticulous
2. Serves them on a temporary URL
3. Runs tests against that URL
4. Reports diffs back to your PR

**Build output**: CRA builds to `build/` directory by default.

### Rewrites Configuration

The `rewrites` parameter handles client-side routing:

```json
[
  { "source": "/(.*)", "destination": "/index.html" }
]
```

This ensures all routes serve `index.html`, allowing React Router to handle routing.

---

## Common Patterns

### Pattern 1: Detect Test Mode

Use `window.Meticulous.isRunningAsTest` in your components:

```typescript
function MyComponent() {
  const isTest = window.Meticulous?.isRunningAsTest

  if (isTest) {
    // Skip animations, use test data, etc.
  }

  return <div>...</div>
}
```

### Pattern 2: Bypass Authentication

```typescript
// In src/App.js or auth provider
import { useEffect } from 'react'

function App() {
  useEffect(() => {
    if (window.Meticulous?.isRunningAsTest) {
      // Mock authentication for tests
      localStorage.setItem('auth-token', 'test-token')
      localStorage.setItem('user', JSON.stringify({
        id: 'test-user',
        name: 'Test User',
        email: 'test@example.com'
      }))
    }
  }, [])

  return <YourApp />
}
```

### Pattern 3: Handle Environment Variables

CRA uses `REACT_APP_` prefix for environment variables:

```typescript
const apiUrl = process.env.REACT_APP_API_URL

// Use different URL for tests
const effectiveUrl = window.Meticulous?.isRunningAsTest
  ? 'https://api.test.example.com'
  : apiUrl
```

**In workflow**:
```yaml
- name: Build app
  run: npm run build
  env:
    REACT_APP_API_URL: "https://api.example.com"
    NODE_ENV: production
    CI: false
```

### Pattern 4: Mock Service Worker for APIs

Use MSW to mock API calls:

```typescript
// src/mocks/browser.js
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

// src/index.js
if (window.Meticulous?.isRunningAsTest && 'serviceWorker' in navigator) {
  const { worker } = await import('./mocks/browser')
  await worker.start()
}

ReactDOM.render(<App />, document.getElementById('root'))
```

---

## Complete Example

### File Structure

```
your-app/
├── public/
│   └── index.html            # Recorder installation
├── src/
│   ├── index.js              # Entry point
│   ├── App.js                # Main app component
│   ├── components/
│   ├── lib/
│   │   └── auth.js           # Auth utilities
│   └── mocks/                # MSW mocks (optional)
├── .github/
│   └── workflows/
│       └── meticulous.yml    # CI/CD
├── package.json
└── .env                      # Environment variables
```

### Example: App with Authentication

**File**: `src/App.js`

```jsx
import { useEffect, useState } from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { checkAuth } from './lib/auth'

function App() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Mock auth for tests
    if (window.Meticulous?.isRunningAsTest) {
      setUser({
        id: 'test-user-123',
        name: 'Test User',
        email: 'test@example.com'
      })
      setLoading(false)
      return
    }

    // Normal auth flow
    checkAuth().then(user => {
      setUser(user)
      setLoading(false)
    }).catch(() => {
      setUser(null)
      setLoading(false)
    })
  }, [])

  if (loading) {
    return (
      <div className="loading">
        <p>Loading...</p>
      </div>
    )
  }

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route
          path="/dashboard"
          element={user ? <Dashboard user={user} /> : <Navigate to="/login" />}
        />
        <Route path="/login" element={<LoginPage />} />
      </Routes>
    </BrowserRouter>
  )
}

export default App
```

---

## CI/CD Configuration Details

### Environment Variables

CRA environment variables must be prefixed with `REACT_APP_`:

```yaml
- name: Build app
  run: npm run build
  env:
    REACT_APP_API_URL: "https://api.example.com"
    REACT_APP_APP_NAME: "My App"
    NODE_ENV: production
    CI: false
```

**In code**:
```javascript
const apiUrl = process.env.REACT_APP_API_URL
```

### Custom Build Script

If you have a custom build script:

```yaml
- name: Build app
  run: npm run build:custom
  env:
    NODE_ENV: production
```

### Using Yarn

If your project uses Yarn:

```yaml
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'yarn'

- name: Install dependencies
  run: yarn install --frozen-lockfile

- name: Build app
  run: yarn build
```

---

## Troubleshooting

### Issue: Build Fails with Warnings

**Symptom**: Build fails in CI with ESLint or TypeScript warnings

**Cause**: CRA treats warnings as errors when `CI=true`

**Fix**: Set `CI=false` in build step:

```yaml
- name: Build app
  run: npm run build
  env:
    CI: false
    NODE_ENV: production
```

**Alternative**: Fix the warnings properly

---

### Issue: Recorder Not Loading

**Symptom**: `window.Meticulous` is undefined

**Checks**:
1. Verify recorder script is in `public/index.html` `<head>`
2. Check project ID is correct
3. Check for CSP blocking (console errors)

**Fix**: Ensure correct placement in `public/index.html`:

```html
<head>
  <!-- Recorder FIRST -->
  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>

  <!-- Then other head elements -->
  <title>Your App</title>
</head>
```

---

### Issue: Routes Return 404

**Symptom**: Direct navigation to routes like `/about` returns 404

**Cause**: Missing rewrite configuration

**Fix**: Ensure rewrites in workflow:

```yaml
rewrites: |
  [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
```

---

### Issue: Environment Variables Not Working

**Symptom**: `process.env.REACT_APP_API_URL` is undefined

**Common causes**:
1. Variable not prefixed with `REACT_APP_`
2. Not added to workflow `env:` section
3. Trying to access in workflow but not in code

**Fix**:

```yaml
# In workflow
- name: Build app
  run: npm run build
  env:
    REACT_APP_API_URL: "https://api.example.com" # Must have REACT_APP_ prefix
```

```javascript
// In code
const apiUrl = process.env.REACT_APP_API_URL
console.log('API URL:', apiUrl) // Should log the URL
```

---

### Issue: API Calls Fail

**Symptom**: API requests fail during tests

**Cause**: No backend in upload-assets mode

**Solutions**:

**Option 1: Mock APIs with MSW** (recommended — see Pattern 4 above)

Keeps you on `upload-assets`, which is the simplest and most reliable workflow.

**Option 2: Switch to `upload-container`** (if you have a backend you want to actually run)

Build a Docker image that runs your backend (and optionally your frontend), and switch the workflow to `upload-container`:

```yaml
- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    tags: my-app:${{ github.sha }}
    platforms: linux/amd64
    push: false
    load: true

- name: Run Meticulous tests
  uses: alwaysmeticulous/report-diffs-action/upload-container@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    image-tag: my-app:${{ github.sha }}
    container-port: 3000
```

---

### Issue: False Positive Diffs

**Symptom**: Tests show diffs for unchanged content

**Common causes**:
1. Animations not completing
2. Random keys or IDs
3. Timestamps or dynamic data

**Fixes**:

**Animations**: Disable in tests
```javascript
const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300
```

**Random IDs**: Use deterministic values
```javascript
const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return Math.random().toString(36).substr(2, 9)
}
```

**Timestamps**: Add `meticulous-ignore` class
```jsx
<span className="meticulous-ignore">
  Last updated: {new Date().toLocaleString()}
</span>
```

Learn more: [Fix False Positive Diffs](/docs/how-to/fix-false-positive-diffs)

---

## Testing Best Practices

### 1. Test Build Locally

Before pushing to CI:

```bash
# Build your app
npm run build

# Serve built files
npx serve -s build

# Verify it works at http://localhost:3000
```

### 2. Handle Loading States Properly

Ensure loading states complete before rendering content:

```jsx
if (loading) {
  return <div className="loading">Loading...</div>
}

if (error) {
  return <div className="error">Error: {error.message}</div>
}

return <div>{/* Your content */}</div>
```

### 3. Use Consistent Placeholders

Instead of spinners, use skeleton screens for consistent layouts:

```jsx
if (loading) {
  return <SkeletonCard />
}
```

---

## Advanced Configuration

### Monorepo Setup

If your CRA app is in a subdirectory:

```yaml
- name: Install dependencies
  working-directory: ./apps/frontend
  run: npm ci

- name: Build app
  working-directory: ./apps/frontend
  run: npm run build
  env:
    CI: false

- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "./apps/frontend/build"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]
```

### Custom Public Path

If you serve your app from a subdirectory:

**In `package.json`**:
```json
{
  "homepage": "/my-app"
}
```

**In workflow**:
```yaml
rewrites: |
  [
    { "source": "/my-app/(.*)", "destination": "/index.html" }
  ]
```

---

## Migration to Modern Tools

CRA is no longer actively maintained. Consider migrating to:

- **Vite**: Faster builds, modern tooling
- **Next.js**: Server-side rendering, better performance
- **Remix**: Full-stack framework

Migration resources:
- [Vite Migration Guide](https://vitejs.dev/guide/migration.html)
- [Next.js Migration Guide](https://nextjs.org/docs/migrating/from-create-react-app)

---

## See Also

- [Onboarding Guide](/docs/onboarding-guide) - General Meticulous setup
- [Troubleshoot Authentication](/docs/how-to/troubleshoot-auth) - Auth patterns and solutions
- [Fix False Positives](/docs/how-to/fix-false-positive-diffs) - Handle non-deterministic content
- [React with Vite](/docs/frameworks/react/vite) - Modern alternative to CRA
