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:

Note: Create React App is no longer actively maintained. For new projects, consider using 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

<!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

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 → 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:

[
  { "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:

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

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

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

Pattern 2: Bypass Authentication

// 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:

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:

- 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:

// 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

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_:

- 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:

const apiUrl = process.env.REACT_APP_API_URL

Custom Build Script

If you have a custom build script:

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

Using Yarn

If your project uses Yarn:

- 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:

- 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:

<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:

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:

# In workflow
- name: Build app
  run: npm run build
  env:
    REACT_APP_API_URL: "https://api.example.com" # Must have REACT_APP_ prefix
// 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:

- 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

Option 3: Switch to cloud-compute (last resort — keeps a server alive in CI)

- name: Start backend
  run: npm run start:api &

- name: Start frontend
  run: npm start &
  env:
    PORT: 3000

- name: Wait for app
  run: npx wait-on http://localhost:3000

- name: Run Meticulous tests
  uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-url: "http://localhost: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

const animationDuration = window.Meticulous?.isRunningAsTest ? 0 : 300

Random IDs: Use deterministic values

const generateId = () => {
  if (window.Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return Math.random().toString(36).substr(2, 9)
}

Timestamps: Add meticulous-ignore class

<span className="meticulous-ignore">
  Last updated: {new Date().toLocaleString()}
</span>

Learn more: Fix False Positive Diffs


Testing Best Practices

1. Test Build Locally

Before pushing to CI:

# 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:

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:

if (loading) {
  return <SkeletonCard />
}

Advanced Configuration

Monorepo Setup

If your CRA app is in a subdirectory:

- 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:

{
  "homepage": "/my-app"
}

In workflow:

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:


See Also