Building a React App with Edge Delivery Services

React and Edge Delivery Services work together surprisingly well, giving developers the best of both worlds - modern component architecture and headless content management.

Bringing React to Adobe's Edge Platform

You might already have a complex React application that your client wants to include in Edge Delivery Services. Perhaps as a separate page, perhaps as a block within existing content. This guide shows you how to make that integration work smoothly.

The complete code lives in the react-with-eds GitHub repository if you want to jump straight in.

What We're Building

This tutorial walks through creating a React slide builder application that integrates with Adobe Edge Delivery Services. The application fetches slide content dynamically and provides an interactive viewing experience with modal panels for detailed content.

We'll use existing JSON data built with EDS, taken from my earlier post on building headless applications with Edge Delivery Services.

The finished application includes a responsive slide gallery with dynamic image loading, interactive modal panels for detailed slide content, smooth integration with Edge Delivery Services, and a production-ready React setup.

Why Choose React for Edge Delivery Services

React brings several advantages to Edge Delivery Services development. Component reusability means you create modular, maintainable pieces. The Virtual DOM provides efficient rendering and updates for better performance. You get access to a vast ecosystem of components and tools, backed by extensive community support and resources. The declarative syntax makes code clear and predictable.

Understanding JSX in React

Before we look at components, let's understand JSX (JavaScript XML). This syntax extension makes React code more readable and easier to write.

JSX allows you to write HTML-like code directly in JavaScript files. During the build process, it transforms into regular JavaScript. Here's a simple example:

// This is JSX
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}

// This is what it compiles to
function Greeting({ name }) {
return React.createElement('h1', null, 'Hello, ', name, '!');
}

Our application uses several key JSX features. We combine HTML-like tags with JavaScript expressions:

<div className="slide-builder-item" style={{ backgroundImage: `url(${bgImage})` }}>
<h2>{slideData.title}</h2>
</div>

Component composition works like HTML tags:

<SlideBuilder slides={slides} />
<SlidePanel onClose={() => setShowPanel(false)} />

We pass data to components through props:

<SlideItem
key={slide.path}
slideData={slide}
index={index}
/>

Conditional rendering uses JavaScript expressions:

{supportingText && (
<p className="supporting-text">{supportingText}</p>
)}

Event handling connects to JavaScript functions:

<button onClick={() =>
setShowPanel(true)}>View Details</button>

Remember these JSX rules: use className instead of class for CSS classes, close all tags (<img /> not <img>), put JavaScript expressions inside curly braces {}, return a single root element from components, and use camelCase for all attributes.

Application Architecture

Our slide builder consists of three main components. The SlideBuilder serves as the main container that fetches and organises slide data. Individual SlideItem components display slides with image loading and click handling. The SlidePanel modal component handles detailed slide content viewing.

Setting Up the React Project

You can clone the complete project structure with git clone https://github.com/ddttom/react-with-eds.git, or start fresh by creating a new React project:

# Create new React project

npx create-react-app react-with-eds
cd react-with-eds
npm install

Organise your project with clear separation of concerns:

/react-with-eds
├── public/
│   ├── index.html      # Development entry point
│   └── slides.html     # Production entry point
├── src/
│   ├── components/     # React components
│   │   ├── SlideBuilder.jsx  # Main slide container component
│   │   ├── SlideItem.jsx     # Individual slide component
│   │   └── SlidePanel.jsx    # Modal panel component
│   ├── styles/         # Component-specific styles
│   │   ├── SlideBuilder.css
│   │   ├── SlideItem.css
│   │   └── SlidePanel.css
│   ├── tests/          # Test files
│   │   ├── components/       # Component tests
│   │   │   ├── SlideBuilder.test.jsx
│   │   │   ├── SlideItem.test.jsx
│   │   │   └── SlidePanel.test.jsx
│   │   └── App.test.jsx     # Application tests
│   ├── config.js       # Environment configuration
│   ├── App.jsx         # Main application component
│   └── index.jsx       # Application entry point
├── blocks/             # Edge Delivery Services blocks
│   └── react-slide-builder/
│       ├── react-slide-builder.js
│       └── react-slide-builder.css
├── config-overrides.js # Custom webpack configuration
└── package.json        # Project configuration

Project Setup and Configuration

HTML Entry Points

The project uses two HTML files to handle different environments:

  1. public/index.html - Development Entry Point
    • Used when running npm start
    • Serves the React app at http://localhost:3000
    • Contains development-specific settings and metadata
    • Enables hot reloading and development tools
  2. public/slides.html - Production Entry Point
    • Used as the template for production builds
    • Gets copied to your blog's directory as slides.html during deployment
    • Contains production-optimized settings
    • Includes only necessary metadata for Edge Delivery Services

When you run npm run build, the build process:

  1. Uses slides.html as the template
  2. Creates optimized production files in the build directory
  3. These files then get copied to your blog's directory using the deployment commands:
cd your-eds-repository
# Copy assets
cp -r ../react-with-eds/build/static/* ./static/
cp ../react-with-eds/build/index.html ./slides.html

# Copy blocks for later
cp -r ../react-with-eds/blocks/* blocks/

Deploy with:

git add .
git commit -m "Add React slide builder application"
git push

This separation allows for different configurations between development and production environments while maintaining a clean deployment process. The development environment gets all the benefits of hot reloading and development tools, while the production build is optimized for Edge Delivery Services deployment.

Environment setup

To handle different environments (development vs production), we use a configuration file:

// src/config.js

const config = {
// Use the full URL in development, relative path in production
baseUrl: process.env.NODE_ENV === 'development'
? 'https://allabout.network'
: '',
};

export default config;

This allows us to use the full URL (https://allabout.network/slides/query-index.json) in development, switch to relative paths (/slides/query-index.json) in production, and change automatically based on the environment.

Our package.json includes all necessary dependencies and scripts:

{
"name": "react-with-eds",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
},

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"lint": "eslint src/**/*.{js,jsx}",
"lint:fix": "eslint src/**/*.{js,jsx} --fix"
},

"eslintConfig": {
"extends": [
"react-app",
"react-app/jest",
"airbnb"
]
},
"proxy": "https://allabout.network"
}

This configuration provides modern React with hooks, Airbnb style guide compliance, development and production builds, code quality tools, and proxy setup for local development.

The development workflow starts with installation:

git clone https://github.com/ddttom/react-with-eds.git
cd react-with-eds
npm install

For development, run npm start to launch the app with hot reloading. Build with npm run build for an optimised production version. Check code quality with npm run lint and fix issues automatically with npm run lint:fix.

Testing Strategy

Our React application includes comprehensive testing using Jest and React Testing Library. This ensures components work as expected and maintain functionality as we make changes.

Our testing approach covers component testing (verifying rendering, user interactions, state management, and props handling), async operation testing (mocking API calls, testing loading states, verifying error handling, and checking data updates), error handling (network error scenarios, invalid data handling, user feedback verification, and recovery mechanisms), and best practices (tests alongside components, clear test descriptions, isolated test cases, and proper cleanup).

Run tests in watch mode with npm test or get coverage reports with npm test -- --coverage. Tests are in the github repo.

The Main App

The App component handles loading and error states:

function App() {
  const [slides, setSlides] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchSlides();
  }, []);

  const fetchSlides = async () => {
    try {
      setIsLoading(true);
      setError(null);
      
      const response = await fetch(`${config.baseUrl}/slides/query-index.json`);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const json = await response.json();
      setSlides(json.data);
    } catch (error) {
      console.error("Failed to fetch slides:", error);
      setError("Failed to fetch slides. Please try again later.");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="App">
      <main>
        {isLoading && <div>Loading...</div>}
        {error && <div>{error}</div>}
        {!isLoading && !error && <SlideBuilder slides={slides} />}
      </main>
    </div>
  );
}

These include loading state management, error handling and display, clean component structure, proper async/await usage, and environment-aware configuration.

Building the Components

The root App component demonstrates several key React and JSX concepts:

import React, { useState, useEffect } from 'react';
import SlideBuilder from './components/SlideBuilder';

import './App.css';
function App() {
// State management with useState hook
const [slides, setSlides] = useState([]);
// Side effects with useEffect hook
useEffect(() => {
fetchSlides();
}, []);

// Async data fetching with proper error handling
const fetchSlides = async () => {
try {
const response = await fetch("/slides/query-index.json");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const json = await response.json();
setSlides(json.data);
} catch (error) {
console.error("Failed to fetch slides:", error);
}
};

// JSX return with component composition

return (
<div className="App">
<main>
<SlideBuilder slides={slides} />
</main>
</div>
);
}

export default App;

This demonstrates hooks (useState for state management, useEffect for side effects), async/await for clean asynchronous code, error handling with proper response validation, component composition using custom components in JSX, props for passing data between components, and JSX syntax with HTML-like structure containing JavaScript expressions.

The container component renders the slide collection:

import React from 'react';
import SlideItem from './SlideItem';
import '../styles/SlideBuilder.css';

function SlideBuilder({ slides }) {
  return (
    <div className="slide-builder">
      {slides.map((slide, index) => (
        <SlideItem 
          key={slide.path} 
          slideData={slide} 
          index={index} 
        />
      ))}
    </div>
  );
}

export default SlideBuilder;

This highlights React features including type-safe component communication through props, efficient list rendering with array mapping, and optimised React reconciliation using the key prop.

Each slide handles its own state, image loading, and content fetching:

import React, { useState, useEffect, useCallback } from 'react';
import SlidePanel from './SlidePanel';
import '../styles/SlideItem.css';

function SlideItem({ slideData, index }) {
  const [bgImage, setBgImage] = useState('');
  const [supportingText, setSupportingText] = useState('');
  const [showPanel, setShowPanel] = useState(false);

  const setSlideBackground = useCallback(() => {
    const imageUrl = slideData.image.split('?')[0];
    const finalImageUrl = `${imageUrl}?width=2000&format=webply&optimize=medium`;
    
    const img = new Image();
    img.onload = () => setBgImage(finalImageUrl);
    img.onerror = () => console.error(`Failed to load image: ${finalImageUrl}`);
    img.src = finalImageUrl;
  }, [slideData.image]);

  const fetchSupportingText = useCallback(async () => {
    if (window.innerWidth <= 799) return;
    
    try {
      const response = await fetch(`${slideData.path}.plain.html`);
      if (!response.ok) return;
      
      const html = await response.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(html, 'text/html');
      
      const h2 = doc.querySelector('h2');
      let firstParagraph = h2 ? h2.nextElementSibling : doc.querySelector('p');
      
      while (firstParagraph && firstParagraph.tagName.toLowerCase() !== 'p') {
        firstParagraph = firstParagraph.nextElementSibling;
      }
      
      setSupportingText(firstParagraph?.textContent.trim() || null);
    } catch (error) {
      console.error('Failed to fetch supporting text:', error);
    }
  }, [slideData.path]);

  useEffect(() => {
    setSlideBackground();
    fetchSupportingText();
  }, [setSlideBackground, fetchSupportingText]);

  return (
    <>
      <div
        className="slide-builder-item"
        style={{ backgroundImage: `url(${bgImage})` }}
        data-slidenum={index + 1}
        onClick={() => setShowPanel(true)}
      >
        <div className="text-container">
          <h2>{slideData.title}</h2>
          <p><strong>{slideData.description}</strong></p>
          {supportingText && <p className="supporting-text">{supportingText}</p>}
        </div>
      </div>
      
      {showPanel && (
        <SlidePanel
          slideData={slideData}
          onClose={() => setShowPanel(false)}
        />
      )}
    </>
  );
}

export default SlideItem;

The modal component handles detailed content viewing:

import React, { useState, useEffect, useRef } from 'react';
import '../styles/SlidePanel.css';

function SlidePanel({ slideData, onClose }) {
  const [html, setHtml] = useState('');
  const panelRef = useRef(null);

  useEffect(() => {
    const fetchSlideHtml = async () => {
      try {
        const response = await fetch(`${slideData.path}.plain.html`);
        
        if (!response.ok) {
          throw new Error(`Failed to fetch HTML for slide: ${slideData.path}`);
        }
        
        const text = await response.text();
        setHtml(text);
      } catch (error) {
        console.error(error);
      }
    };

    fetchSlideHtml();

    const handleOutsideClick = (event) => {
      if (panelRef.current && !panelRef.current.contains(event.target)) {
        onClose();
      }
    };

    document.addEventListener('mousedown', handleOutsideClick);
    
    return () => document.removeEventListener('mousedown', handleOutsideClick);
  }, [slideData.path, onClose]);

  return (
    <div 
      className="slide-panel-overlay" 
      onClick={(e) => e.target === e.currentTarget && onClose()}
    >
      <div className="slide-panel" ref={panelRef}>
        <div className="slide-panel-content">
          <div 
            className="slide-panel-body" 
            dangerouslySetInnerHTML={{ __html: html }} 
          />
          <button 
            className="slide-panel-close" 
            onClick={onClose} 
            aria-label="Close panel"
          >
            &times;
          </button>
        </div>
      </div>
    </div>
  );
}

export default SlidePanel;

Component Communication Patterns

React and JSX provide elegant patterns for component communication. For parent to child data flow, we use props:

// Parent component
function ParentComponent() {
  const slide = {
    title: "Example Slide",
    description: "This is an example slide",
    image: "/path/to/image.jpg"
  };
  const index = 0;

  return (
    <div>
      <SlideItem slideData={slide} index={index} />
    </div>
  );
}

// Child component receiving props
function SlideItem({ slideData, index }) {
  // Use props in JSX
  return (
    <div className="slide-item">
      <h2>{slideData.title}</h2>
      <p>{slideData.description}</p>
      <span>Slide {index + 1}</span>
    </div>
  );
}

For child to parent communication, we use callbacks:

import React, { useState } from 'react';

// Parent component
function SlideBuilder() {
  const [showPanel, setShowPanel] = useState(true);
  
  const handleClose = () => setShowPanel(false);
  
  return (
    <div>
      {showPanel && (
        <SlidePanel onClose={handleClose} />
      )}
    </div>
  );
}

// Child component
function SlidePanel({ onClose }) {
  return (
    <div className="slide-panel">
      <h2>Slide Panel Content</h2>
      <button onClick={onClose}>
        Close
      </button>
    </div>
  );
}

State management with hooks keeps everything organised:

 const [showPanel, setShowPanel] = useState(false);

  return (
    <div>
      <button onClick={() => setShowPanel(true)}>
        Open Panel
      </button>
      
      {/* Using state in JSX - Conditional Rendering */}
      {showPanel && (
        <SlidePanel 
          onClose={() => setShowPanel(false)} 
        />
      )}
    </div>
  );
}

Styling Architecture

React supports multiple styling approaches. We use modular CSS files for maintainability:

import '../styles/SlideItem.css';

The modular approach allows each component to manage its own styles while sharing common design patterns. All CSS files are available in the styles directory.

Edge Delivery Services Integration

There are two main approaches for integration. The standalone page integration is recommended for most use cases.

For standalone page integration, first build for production:

#build
npm run build

Then prepare your EDS repository. Navigate to your existing Edge Delivery Services repository and create the necessary structure:

cd your-eds-repository
# Copy assets
cp -r ../react-with-eds/build/static/* ./static/
cp ../react-with-eds/build/index.html ./slides.html

# Copy blocks for later
cp -r ../react-with-eds/blocks/* blocks/

Deploy with:

git add .
git commit -m "Add React slide builder application"
git push

Your React app will be available at https://your-domain.com/slides.html. My React app is here: https://allabout.network/slides.html.

Create a React block for in-page React

For deeper integration where the React app appears as a block within EDS pages, create the block structure:

#
mkdir -p blocks/react-slide-builder

Create blocks/react-slide-builder/react-slide-builder.js:

export default function decorate(block) {

// Create container for React app
const container = document.createElement('div');
container.id = 'react-slide-app';
block.appendChild(container);

// Load React bundle

const script = document.createElement('script');
script.src = '/static/js/slide-builder-main.js'; // Your built JS file
script.type = 'module';

document.head.appendChild(script);

// Load CSS

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/static/css/slide-builder-main.css'; // Your built CSS file
document.head.appendChild(link);
}

Create blocks/react-slide-builder/react-slide-builder.css:

.react-slide-builder {
width: 100%;
min-height: 400px;
}

.react-slide-builder #react-slide-app {
width: 100%;
}

We have modified the standard React index.jsx to check for the block container:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './app.jsx';

const mountPoint = document.getElementById('react-slide-app') || document.getElementById('root');

if (mountPoint) {
  ReactDOM.createRoot(mountPoint).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
}

In your Document, add the block:

I've used the block here: https://allabout.network/blogs/ddt/integrations/reactjs-version-of-slide-builder

Development Experience Benefits

React brings several advantages to Edge Delivery Services development. Component-based architecture creates reusable, maintainable components. The Virtual DOM provides efficient rendering and updates. You get access to a rich ecosystem with a vast library of tools and components. Strong community support offers extensive resources. The declarative syntax ensures clear, predictable code structure.

Deployment Considerations

The application automatically handles different environments. In development mode (npm start), it uses the full URL for API calls, enables hot reloading, shows detailed error messages, and includes development tools. In production mode (npm run build), it uses relative paths, provides optimised and minified code, removes development tools, and delivers better performance.

When deploying to Edge Delivery Services, build the application with npm run build, copy the build files with cp -r build/* your-eds-repository/, and the application will automatically use the correct API endpoints based on the environment.

To work within CORS restrictions, use relative paths for all requests in production deployment, deploy the application to the same domain, and ensure all requests are same-origin. In the development environment, use the proxy configuration in package.json to make requests appear from the same origin while avoiding CORS issues.

This approach ensures full compliance with the Edge Delivery Services security policy, smooth operation in both development and production, no need to modify the CSP, and secure communication between the application and the CMS.

Development Configuration

To handle CORS issues during local development, we use a proxy configuration in package.json:

{
"proxy": "https://allabout.network"
}

This setup allows us to use relative paths in our code, let the development server proxy requests to the Edge Delivery Services instance, avoid CORS issues during local development, and keep the same code structure for both environments.

The application uses a simple configuration in src/config.js:

const config = {
baseUrl: '', // Empty string for relative paths in both environments
};

export default config;

This configuration works in both environments. In development, the proxy handles forwarding requests to the full URL. In production, direct relative paths are used since the app is deployed to the same domain.

The development workflow starts by running npm start. The application will be available at http://localhost:3000. All API requests will be proxied to the Edge Delivery Services instance with no CORS issues and hot reloading for instant feedback.

When building for production, run npm run build and deploy the contents of the build directory. The application will use relative paths with no proxy needed since everything is on the same domain. The build process generates non-hashed filenames (slide-builder-main.js and slide-builder-main.css) for easier integration and no license file, making deployment simpler.

This approach provides a clean development experience without CORS issues, simple production deployment, consistent code between environments, and no environment-specific code changes needed.

Conclusion

Building a React slide builder with Edge Delivery Services demonstrates the power of combining modern frontend frameworks with headless content management. This approach provides developer productivity through intuitive React patterns, content flexibility allowing authors to work in familiar document environments, performance through optimised builds and progressive loading, maintainability with clear component structure and separation of concerns, and scalability for easy extension with additional features and integrations.

The React ecosystem offers excellent tooling and community support, making it an ideal choice for Edge Delivery Services applications that require interactive, dynamic user interfaces.

Clone the complete project and customise it for your needs.

This pattern works excellently for portfolios, product showcases, educational content, marketing campaigns, and any scenario where you need rich, interactive content management with a modern development experience.

Resources and Next Steps

For further learning, explore the React documentation, Adobe Edge Delivery Services, Create React App, and the project repository.

Found this helpful? Star the https://github.com/ddttom/react-with-eds on GitHub!

/fragments/ddt/proposition

Related Articles

path=*
path=*
Back to Top