React Integration

Integrate Headless Bridge with your React application (Create React App, Vite, or any React setup).

Quick Start

1. Environment Variables

Create a .env file:

REACT_APP_WP_API_URL=https://your-wordpress-site.com/wp-json/bridge/v1
REACT_APP_WP_API_KEY=your_api_key_here  # Optional, for preview content

For Vite, use VITE_ prefix instead:

VITE_WP_API_URL=https://your-wordpress-site.com/wp-json/bridge/v1
VITE_WP_API_KEY=your_api_key_here

2. API Client

Create src/services/wordpress-client.js:

const API_URL = process.env.REACT_APP_WP_API_URL || 
                process.env.VITE_WP_API_URL;
const API_KEY = process.env.REACT_APP_WP_API_KEY || 
                process.env.VITE_WP_API_KEY;

async function apiFetch(endpoint, preview = false) {
  const headers = { 'Content-Type': 'application/json' };
  if (API_KEY && preview) {
    headers['X-Headless-Bridge-Key'] = API_KEY;
  }

  const response = await fetch(`${API_URL}${endpoint}`, { headers });
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

export async function getPostBySlug(slug, preview = false) {
  return apiFetch(
    `/page?slug=${encodeURIComponent(slug)}${preview ? '&preview=true' : ''}`,
    preview
  );
}

export async function getPosts(options = {}) {
  const { type = 'post', limit = 20, offset = 0 } = options;
  const params = new URLSearchParams({ type, limit, offset });
  return apiFetch(`/pages?${params}`);
}

Usage Examples

Blog Post List

import { useState, useEffect } from 'react';
import { getPosts } from './services/wordpress-client';

function BlogList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getPosts({ type: 'post', limit: 10 })
      .then(data => {
        setPosts(data.items);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;

  return (
    <div className="blog-list">
      {posts.map(post => (
        <article key={post.uuid}>
          {post.featuredImage && (
            <img 
              src={post.featuredImage.url} 
              alt={post.featuredImage.alt}
            />
          )}
          <h2>{post.title}</h2>
          <div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
          <a href={`/blog/${post.slug}`}>Read More</a>
        </article>
      ))}
    </div>
  );
}

Single Post

import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { getPostBySlug } from './services/wordpress-client';

function BlogPost() {
  const { slug } = useParams();
  const [post, setPost] = useState(null);

  useEffect(() => {
    getPostBySlug(slug).then(setPost);
  }, [slug]);

  if (!post) return <div>Loading...</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {post.author && (
        <div className="author">
          <img src={post.author.avatar} alt={post.author.name} />
          <span>{post.author.name}</span>
        </div>
      )}
      <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Custom Hook

Create reusable hooks for cleaner code:

// hooks/useWordPress.js
import { useState, useEffect } from 'react';
import { getPostBySlug, getPosts } from '../services/wordpress-client';

export function usePost(slug) {
  const [post, setPost] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!slug) return;
    getPostBySlug(slug)
      .then(setPost)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [slug]);

  return { post, loading, error };
}

export function usePosts(options = {}) {
  const [posts, setPosts] = useState([]);
  const [pagination, setPagination] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getPosts(options)
      .then(data => {
        setPosts(data.items);
        setPagination(data.pagination);
        setLoading(false);
      });
  }, [JSON.stringify(options)]);

  return { posts, pagination, loading };
}

// Usage:
// const { post, loading } = usePost('my-post-slug');
// const { posts, pagination } = usePosts({ type: 'post', limit: 10 });

React Router Setup

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import BlogList from './components/BlogList';
import BlogPost from './components/BlogPost';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<BlogList />} />
        <Route path="/blog/:slug" element={<BlogPost />} />
      </Routes>
    </BrowserRouter>
  );
}

Pagination

function PaginatedBlogList() {
  const [posts, setPosts] = useState([]);
  const [pagination, setPagination] = useState({ 
    total: 0, offset: 0, limit: 10 
  });

  const loadPosts = (offset = 0) => {
    getPosts({ type: 'post', limit: 10, offset })
      .then(data => {
        setPosts(data.items);
        setPagination(data.pagination);
      });
  };

  useEffect(() => loadPosts(0), []);

  const currentPage = Math.floor(pagination.offset / pagination.limit) + 1;
  const totalPages = Math.ceil(pagination.total / pagination.limit);

  return (
    <div>
      <div className="posts">
        {posts.map(post => (
          <article key={post.uuid}>
            <h2>{post.title}</h2>
          </article>
        ))}
      </div>
      <div className="pagination">
        <button onClick={() => loadPosts(pagination.offset - pagination.limit)}>
          Previous
        </button>
        <span>Page {currentPage} of {totalPages}</span>
        <button onClick={() => loadPosts(pagination.offset + pagination.limit)}>
          Next
        </button>
      </div>
    </div>
  );
}

SEO with React Helmet

Install: npm install react-helmet-async

import { Helmet } from 'react-helmet-async';

function BlogPost({ slug }) {
  const { post } = usePost(slug);

  return (
    <>
      <Helmet>
        <title>{post.seo?.title || post.title}</title>
        <meta name="description" content={post.seo?.description} />
        <meta property="og:title" content={post.seo?.title} />
        <meta property="og:image" content={post.featuredImage?.url} />
        <link rel="canonical" href={post.seo?.canonical} />
      </Helmet>
      <article>
        {/* Post content */}
      </article>
    </>
  );
}

React Query Integration

For advanced caching and state management:

import { useQuery } from '@tanstack/react-query';
import { getPostBySlug } from './services/wordpress-client';

function usePost(slug) {
  return useQuery({
    queryKey: ['post', slug],
    queryFn: () => getPostBySlug(slug),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

// Usage:
const { data: post, isLoading, error } = usePost('my-post-slug');

Performance Tips

1. Image Lazy Loading

<img 
  src={post.featuredImage.url}
  alt={post.featuredImage.alt}
  loading="lazy"
  srcSet={post.featuredImage.srcset}
  sizes={post.featuredImage.sizes}
/>

2. Memoization

import { useMemo } from 'react';

const sortedPosts = useMemo(() => {
  return posts.sort((a, b) => 
    new Date(b.publishedAt) - new Date(a.publishedAt)
  );
}, [posts]);

3. Code Splitting

import { lazy, Suspense } from 'react';

const BlogPost = lazy(() => import('./components/BlogPost'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <BlogPost />
    </Suspense>
  );
}

CORS Configuration

If developing locally, add to WordPress wp-config.php:

header('Access-Control-Allow-Origin: http://localhost:3000');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Headless-Bridge-Key');

Or use a proxy in package.json (CRA):

{
  "proxy": "http://localhost:8080"
}

Next Steps

  • Add search functionality
  • Implement infinite scroll
  • Add loading skeletons
  • Cache API responses
  • Add error boundaries
  • Implement offline support

Related Documentation