Skip to content

React Integration Examples

Comprehensive guide to integrating PGRestify with React applications using both PostgREST syntax and repository patterns.

Installation & Setup

bash
npm install @webcoded/pgrestify

Basic Setup

tsx
import React from 'react';
import { createClient } from '@webcoded/pgrestify';
import { PGRestifyProvider } from '@webcoded/pgrestify/react';

// Create PGRestify client
const client = createClient({
  url: 'http://localhost:3000',
  auth: {
    persistSession: true
  }
});

// App component with PGRestify provider
function App() {
  return (
    <PGRestifyProvider client={client}>
      <UserManagementApp />
    </PGRestifyProvider>
  );
}

export default App;

Type Definitions

tsx
interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
  role: 'user' | 'admin' | 'moderator';
  avatar_url?: string;
  created_at: string;
  updated_at?: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  author_id: number;
  published: boolean;
  created_at: string;
  author?: User;
}

Data Fetching with Hooks

Using Built-in React Hooks

PGRestify provides powerful React hooks for data fetching with automatic caching, isLoading states, and error handling.

tsx
import { useQuery } from '@webcoded/pgrestify/react';

function UserList() {
  const { 
    data: users, 
    isLoading, 
    error,
    refetch 
  } = useQuery({
    queryKey: ['users'],
    queryFn: async ({ client }) => {
      const { data, error } = await client
        .from('users')
        .select('*')
        .eq('active', true)
        .order('name', { ascending: true })
        .execute();
      
      if (error) throw error;
      return data;
    }
  });

  if (isLoading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      <ul>
        {users?.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}
tsx
import { useRepository } from '@webcoded/pgrestify/react';

function UserList() {
  const userRepo = useRepository<User>('users');
  
  const { 
    data: users, 
    isLoading, 
    error,
    refetch 
  } = useQuery({
    queryKey: ['users', 'active'],
    queryFn: async () => {
      return await userRepo
        .createQueryBuilder()
        .where('active = :active', { active: true })
        .orderBy('name', 'ASC')
        .getMany();
    }
  });

  if (isLoading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      <ul>
        {users?.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}

Advanced Features: Relations, Aliases, and Multiple Sorting

React Hooks with Relations Array Syntax

tsx
import { useQuery } from '@webcoded/pgrestify/react';

interface UserWithProfile extends User {
  profile?: {
    bio: string;
    avatar_url: string;
    website?: string;
  };
  posts?: Post[];
}

// Using relations array syntax with aliases and multiple sorting
function UserProfileDashboard() {
  const { data: users, isLoading, error } = useQuery<UserWithProfile>({
    queryKey: ['users-with-profiles'],
    queryFn: async ({ client }) => {
      const { data, error } = await client
        .from('users')
        .select([
          'id AS user_id',
          'name AS full_name',
          'email AS contact_email',
          'created_at AS join_date',
          'profile.bio AS user_bio',
          'profile.avatar_url AS profile_image',
          'profile.website AS personal_website',
          'posts.title AS post_titles',
          'posts.created_at AS post_dates'
        ])
        .relations(['profile', 'posts'])
        .eq('active', true)
        .order('profile.created_at', { ascending: false })  // Latest profiles first
        .order('name', { ascending: true })                 // Alphabetical names
        .order('posts.created_at', { ascending: false })    // Latest posts first
        .execute();
      
      if (error) throw error;
      return data;
    }
  });

  if (isLoading) return <div>Loading user profiles...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div className="user-dashboard">
      {users?.map(user => (
        <div key={user.user_id} className="user-card">
          <div className="profile-header">
            <img src={user.profile_image} alt="Profile" />
            <div className="user-info">
              <h3>{user.full_name}</h3>
              <p>{user.contact_email}</p>
              <p>{user.user_bio}</p>
              {user.personal_website && (
                <a href={user.personal_website} target="_blank" rel="noopener">
                  {user.personal_website}
                </a>
              )}
            </div>
          </div>
          <div className="posts-preview">
            <h4>Recent Posts</h4>
            {user.post_titles?.slice(0, 3).map((title, index) => (
              <div key={index} className="post-item">
                <span>{title}</span>
                <time>{new Date(user.post_dates[index]).toLocaleDateString()}</time>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

// E-commerce product catalog with complex relations
function ProductCatalogWithRelations() {
  const { data: products, isLoading } = useQuery({
    queryKey: ['products-catalog'],
    queryFn: async ({ client }) => {
      const { data, error } = await client
        .from('products')
        .select([
          'id AS product_id',
          'name AS product_name',
          'price AS current_price',
          'description AS product_description',
          'category.name AS category_name',
          'category.slug AS category_path',
          'brand.name AS brand_name',
          'brand.logo_url AS brand_logo',
          'reviews.rating AS avg_rating',
          'reviews.count AS review_count',
          'inventory.stock AS available_stock'
        ])
        .relations(['category', 'brand', 'reviews', 'inventory'])
        .eq('active', true)
        .gte('inventory.stock', 1)  // Only in-stock items
        .order('category.sort_order', { ascending: true })     // Category priority
        .order('reviews.rating', { ascending: false })        // Best rated first
        .order('brand.popularity', { ascending: false })      // Popular brands
        .order('price', { ascending: true })                  // Cheapest first
        .limit(50)
        .execute();
      
      if (error) throw error;
      return data;
    }
  });

  return (
    <div className="product-catalog">
      {products?.map(product => (
        <div key={product.product_id} className="product-card">
          <div className="product-header">
            <img src={product.brand_logo} alt={product.brand_name} className="brand-logo" />
            <span className="category-badge">{product.category_name}</span>
          </div>
          
          <div className="product-details">
            <h3>{product.product_name}</h3>
            <p className="description">{product.product_description}</p>
            
            <div className="pricing-info">
              <span className="price">${product.current_price}</span>
              <div className="rating">
                ⭐ {product.avg_rating} ({product.review_count} reviews)
              </div>
            </div>
            
            <div className="inventory-info">
              <span className="stock">{product.available_stock} in stock</span>
              <span className="brand">by {product.brand_name}</span>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

// Blog platform with authors and categories
function BlogPostsWithAuthors() {
  const { data: posts, isLoading } = useQuery({
    queryKey: ['blog-posts'],
    queryFn: async ({ client }) => {
      const { data, error } = await client
        .from('posts')
        .select([
          'id AS post_id',
          'title AS post_title',
          'content AS post_content',
          'excerpt AS post_excerpt',
          'published_at AS publication_date',
          'author.name AS author_name',
          'author.bio AS author_bio',
          'author.avatar_url AS author_avatar',
          'category.name AS category_name',
          'category.color AS category_color',
          'tags.name AS tag_names',
          'comments.count AS comment_count',
          'likes.count AS like_count'
        ])
        .relations(['author', 'category', 'tags', 'comments', 'likes'])
        .eq('published', true)
        .eq('author.active', true)
        .order('category.priority', { ascending: true })       // Featured categories first
        .order('published_at', { ascending: false })          // Latest posts first
        .order('likes.count', { ascending: false })           // Popular posts
        .order('author.reputation', { ascending: false })     // Reputable authors
        .limit(30)
        .execute();
      
      if (error) throw error;
      return data;
    }
  });

  return (
    <div className="blog-posts">
      {posts?.map(post => (
        <article key={post.post_id} className="blog-post">
          <header className="post-header">
            <div 
              className="category-badge" 
              style={{ backgroundColor: post.category_color }}
            >
              {post.category_name}
            </div>
            <h2>{post.post_title}</h2>
            <p className="excerpt">{post.post_excerpt}</p>
          </header>
          
          <div className="author-section">
            <img src={post.author_avatar} alt={post.author_name} />
            <div className="author-info">
              <h4>{post.author_name}</h4>
              <p>{post.author_bio}</p>
            </div>
          </div>
          
          <div className="post-meta">
            <time>{new Date(post.publication_date).toLocaleDateString()}</time>
            <div className="engagement">
              <span>❤️ {post.like_count}</span>
              <span>💬 {post.comment_count}</span>
            </div>
          </div>
          
          <div className="tags">
            {post.tag_names?.map((tag, index) => (
              <span key={index} className="tag">{tag}</span>
            ))}
          </div>
        </article>
      ))}
    </div>
  );
}

Repository Pattern with Advanced Features

tsx
import { useRepository } from '@webcoded/pgrestify/react';

// Team management dashboard using repository pattern
function TeamManagementDashboard() {
  const userRepo = useRepository<User>('users');
  
  const { data: teamMembers, isLoading } = useQuery({
    queryKey: ['team-members'],
    queryFn: async () => {
      return await userRepo
        .createQueryBuilder()
        .select([
          'id AS employee_id',
          'first_name AS firstName',
          'last_name AS lastName', 
          'email AS workEmail',
          'department.name AS dept_name',
          'manager.first_name AS manager_firstName',
          'manager.last_name AS manager_lastName',
          'role.name AS job_title',
          'role.level AS experience_level'
        ])
        .relations(['department', 'manager', 'role'])
        .where('active = :active', { active: true })
        .orderBy('department.name', 'ASC')             // Group by department
        .addOrderBy('manager.last_name', 'ASC')        // Then by manager
        .addOrderBy('role.level', 'DESC')              // Senior roles first
        .addOrderBy('last_name', 'ASC')                // Alphabetical by surname
        .addOrderBy('first_name', 'ASC')               // Then by first name
        .getMany();
    }
  });

  return (
    <div className="team-directory">
      <h2>Team Directory</h2>
      {teamMembers?.map(member => (
        <div key={member.employee_id} className="team-member">
          <div className="member-info">
            <h3>{member.firstName} {member.lastName}</h3>
            <p className="title">{member.job_title}</p>
            <p className="email">{member.workEmail}</p>
          </div>
          <div className="org-info">
            <p className="department">{member.dept_name}</p>
            <p className="manager">
              Reports to: {member.manager_firstName} {member.manager_lastName}
            </p>
            <span className="level">Level {member.experience_level}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

// Analytics dashboard with complex aggregations
function AnalyticsDashboardWithRelations() {
  const { data: analytics, isLoading } = useQuery({
    queryKey: ['analytics-dashboard'],
    queryFn: async ({ client }) => {
      const { data, error } = await client
        .from('analytics')
        .select([
          'date AS report_date',
          'metrics.page_views AS daily_page_views',
          'metrics.unique_visitors AS daily_visitors',
          'metrics.bounce_rate AS visitor_bounce_rate',
          'content.posts_published AS new_posts',
          'content.comments_count AS total_comments',
          'sales.revenue AS daily_revenue',
          'sales.orders AS daily_orders',
          'sales.conversion_rate AS sales_conversion'
        ])
        .relations(['metrics', 'content', 'sales'])
        .gte('date', '2024-01-01')
        .lte('date', '2024-12-31')
        .order('date', { ascending: false })               // Latest first
        .order('metrics.page_views', { ascending: false }) // High traffic days
        .order('sales.revenue', { ascending: false })      // High revenue days
        .limit(90)  // Last 90 days
        .execute();
        
      if (error) throw error;
      return data;
    }
  });

  if (isLoading) return <div>Loading analytics...</div>;

  return (
    <div className="analytics-dashboard">
      <div className="metrics-grid">
        {analytics?.map(day => (
          <div key={day.report_date} className="day-card">
            <h4>{new Date(day.report_date).toLocaleDateString()}</h4>
            
            <div className="traffic-metrics">
              <div className="metric">
                <span className="value">{day.daily_page_views}</span>
                <span className="label">Page Views</span>
              </div>
              <div className="metric">
                <span className="value">{day.daily_visitors}</span>
                <span className="label">Visitors</span>
              </div>
              <div className="metric">
                <span className="value">{day.visitor_bounce_rate}%</span>
                <span className="label">Bounce Rate</span>
              </div>
            </div>
            
            <div className="content-metrics">
              <div className="metric">
                <span className="value">{day.new_posts}</span>
                <span className="label">New Posts</span>
              </div>
              <div className="metric">
                <span className="value">{day.total_comments}</span>
                <span className="label">Comments</span>
              </div>
            </div>
            
            <div className="sales-metrics">
              <div className="metric">
                <span className="value">${day.daily_revenue}</span>
                <span className="label">Revenue</span>
              </div>
              <div className="metric">
                <span className="value">{day.daily_orders}</span>
                <span className="label">Orders</span>
              </div>
              <div className="metric">
                <span className="value">{day.sales_conversion}%</span>
                <span className="label">Conversion</span>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Mutation Hook

tsx
import { useMutation } from '@webcoded/pgrestify/react';

function CreateUserForm() {
  const { 
    mutate: createUser, 
    isLoading, 
    error 
  } = useMutation<User>('users');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const newUser = await createUser({
        name: 'John Doe',
        email: 'john@example.com',
        active: true
      });
      
      console.log('User created:', newUser);
    } catch (submitError) {
      console.error('User creation failed', submitError);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {isLoading && <div>Submitting...</div>}
      {error && <div>Error: {error.message}</div>}
      <button type="submit">Create User</button>
    </form>
  );
}

Pagination Hook

tsx
import { usePaginatedQuery } from '@webcoded/pgrestify/react';

function PaginatedUserList() {
  const { 
    data: users, 
    isLoading, 
    error,
    pagination,
    fetchNextPage,
    fetchPreviousPage
  } = usePaginatedQuery<User>('users', query => 
    query.select('*').order('created_at', { ascending: false })
  );

  return (
    <div>
      {users?.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      
      <div>
        <button 
          disabled={!pagination.hasPreviousPage}
          onClick={fetchPreviousPage}
        >
          Previous
        </button>
        <button 
          disabled={!pagination.hasNextPage}
          onClick={fetchNextPage}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Real-time Subscription Hook

tsx
import { useRealtimeSubscription } from '@webcoded/pgrestify/react';

function LiveUserUpdates() {
  const { 
    data: newUsers, 
    error 
  } = useRealtimeSubscription<User>('users', 'INSERT');

  return (
    <div>
      {newUsers?.map(user => (
        <div key={user.id}>New user: {user.name}</div>
      ))}
    </div>
  );
}

Advanced Query Configuration

tsx
function ComplexUserQuery() {
  const { 
    data: users, 
    isLoading, 
    error 
  } = useQuery<User>('users', query => 
    query
      .select('id', 'name', 'email')
      .eq('active', true)
      .order('created_at', { ascending: false })
      .limit(10)
  );

  // Render logic
}

Error Handling

tsx
function UserQueryWithErrorHandling() {
  const { 
    data: users, 
    isLoading, 
    error,
    retry 
  } = useQuery<User>('users', query => 
    query.select('*')
  );

  if (error) {
    return (
      <div>
        <p>Error isLoading users: {error.message}</p>
        <button onClick={retry}>Retry</button>
      </div>
    );
  }

  // Render users
}

Caching and Performance

tsx
function CachedUserQuery() {
  const { 
    data: users 
  } = useQuery<User>('users', query => 
    query.select('*'), 
    {
      // Cache configuration
      cacheTime: 300000, // 5 minutes
      staleTime: 60000,  // 1 minute
      refetchOnWindowFocus: true
    }
  );

  // Render users
}

Type Safety

tsx
interface ComplexUser {
  id: number;
  name: string;
  email: string;
  posts: {
    id: number;
    title: string;
  }[];
}

function TypeSafeUserQuery() {
  const { 
    data: users 
  } = useQuery<ComplexUser>('users', query => 
    query.select(`
      id,
      name,
      email,
      posts:posts(id, title)
    `)
  );

  // TypeScript ensures type safety
  users?.forEach(user => {
    console.log(user.posts[0].title);
  });
}

Optimistic Updates

tsx
function OptimisticUpdateExample() {
  const { 
    mutate: updateUser,
    optimisticUpdate
  } = useMutation<User>('users');

  const handleUserUpdate = async (userId: number, updates: Partial<User>) => {
    // Optimistic update before server confirmation
    optimisticUpdate(userId, updates);

    try {
      await updateUser(userId, updates);
    } catch (error) {
      // Rollback if server update fails
      optimisticUpdate(userId, null);
    }
  };
}

Authentication Integration

tsx
import { useAuth } from '@webcoded/pgrestify/react';

function AuthenticatedUserProfile() {
  const { 
    user, 
    signOut 
  } = useAuth();

  if (!user) return <LoginForm />;

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <button onClick={signOut}>Logout</button>
    </div>
  );
}

Best Practices

  • Wrap app with PGRestifyProvider
  • Use type generics for type safety
  • Handle isLoading and error states
  • Leverage caching for performance
  • Use hooks for different query types
  • Implement error boundaries

Performance Considerations

  • Hooks are lightweight
  • Minimal re-renders with memoization
  • Automatic caching and deduplication
  • Configurable refetch strategies
  • Supports server-side rendering

Troubleshooting

  • Ensure PGRestifyProvider is set up
  • Check network connectivity
  • Verify PostgREST URL
  • Use error handling in hooks
  • Monitor network tab for query details

Released under the MIT License.