Skip to content

React Examples ​

Complete examples for using Flowfull Clients with React applications.

Architecture

  • @pubflow/react - For authentication with Flowless (managed auth service)
  • @pubflow/flowfull-client - For HTTP requests to your custom Flowfull backend

Installation ​

bash
# Install both packages
npm install @pubflow/react @pubflow/flowfull-client

Basic Setup ​

1. Configure PubflowProvider ​

typescript
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { PubflowProvider } from '@pubflow/react';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Users from './pages/Users';

function App() {
  return (
    <PubflowProvider
      config={{
        baseUrl: import.meta.env.VITE_FLOWLESS_URL || 'https://your-flowless.pubflow.com',
        bridgeBasePath: '/bridge',
        authBasePath: '/auth'
      }}
      loginRedirectPath="/login"
      publicPaths={['/login', '/register']}
    >
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/users" element={<Users />} />
        </Routes>
      </BrowserRouter>
    </PubflowProvider>
  );
}

export default App;

2. Create Flowfull API Client ​

typescript
// src/api/flowfull.ts
import { createFlowfull } from '@pubflow/flowfull-client';

// Create client for your custom backend
export const flowfull = createFlowfull(
  import.meta.env.VITE_BACKEND_URL || 'https://api.myapp.com',
  {
    headers: {
      'X-Client-Version': '1.0.0'
    },
    retryAttempts: 3,
    timeout: 30000
  }
);

Authentication with Flowless ​

Login Page ​

typescript
// src/pages/Login.tsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@pubflow/react';

function Login() {
  const navigate = useNavigate();
  const { login, isLoading } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setError('');

    try {
      const result = await login({ email, password });

      if (result.success) {
        navigate('/dashboard');
      } else {
        setError(result.error || 'Login failed');
      }
    } catch (err) {
      setError('An error occurred');
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div className="login-page">
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value.toLowerCase())}
          placeholder="Email"
          required
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
          required
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Logging in...' : 'Login'}
        </button>
        {error && <div className="error">{error}</div>}
      </form>
    </div>
  );
}

export default Login;

User Profile Display ​

typescript
// src/pages/Dashboard.tsx
import { useAuth } from '@pubflow/react';
import { useNavigate } from 'react-router-dom';

function Dashboard() {
  const { user, isAuthenticated, logout, isLoading } = useAuth();
  const navigate = useNavigate();

  async function handleLogout() {
    await logout();
    navigate('/login');
  }

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

  if (!isAuthenticated) {
    navigate('/login');
    return null;
  }

  return (
    <div className="dashboard">
      <h1>Dashboard</h1>
      <div className="user-info">
        <p><strong>Name:</strong> {user?.name}</p>
        <p><strong>Email:</strong> {user?.email}</p>
        <p><strong>User Type:</strong> {user?.userType}</p>
      </div>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
}

export default Dashboard;

Protected Route with useAuthGuard ​

typescript
// src/pages/AdminPanel.tsx
import { useAuthGuard } from '@pubflow/react';
import { useNavigate } from 'react-router-dom';

function AdminPanel() {
  const navigate = useNavigate();

  const { user, isLoading } = useAuthGuard({
    allowedTypes: ['admin'],
    onRedirect: (path, reason) => {
      if (reason === 'unauthorized') {
        alert('Admin access required');
      }
      navigate(path);
    }
  });

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

  return (
    <div>
      <h1>Admin Panel</h1>
      <p>Welcome, Admin {user?.name}!</p>
    </div>
  );
}

export default AdminPanel;

Data Fetching from Custom Backend ​

Use @pubflow/flowfull-client to fetch data from your custom Flowfull backend:

User List with Pagination ​

typescript
// src/pages/Users.tsx
import { useState, useEffect } from 'react';
import { flowfull } from '../api/flowfull';

interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}

function Users() {
  const [users, setUsers] = useState<User[]>([]);
  const [page, setPage] = useState(1);
  const [total, setTotal] = useState(0);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const limit = 10;

  useEffect(() => {
    fetchUsers();
  }, [page]);

  async function fetchUsers() {
    setIsLoading(true);
    setError(null);

    const response = await api
      .query('/users')
      .where('status', 'active')
      .sort('name', 'asc')
      .page(page)
      .limit(limit)
      .get<User[]>();

    if (response.success) {
      setUsers(response.data || []);
      setTotal(response.meta?.total || 0);
    } else {
      setError(response.error || 'Failed to fetch users');
    }

    setIsLoading(false);
  }

  if (isLoading && users.length === 0) {
    return <div>Loading users...</div>;
  }

  if (error) {
    return <div>Error: {error}</div>;
  }

  const totalPages = Math.ceil(total / limit);

  return (
    <div>
      <h1>Users</h1>
      <button onClick={fetchUsers}>Refresh</button>

      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Type</th>
          </tr>
        </thead>
        <tbody>
          {users.map(user => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.email}</td>
              <td>{user.user_type}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <div className="pagination">
        <button
          onClick={() => setPage(p => p - 1)}
          disabled={page === 1 || isLoading}
        >
          Previous
        </button>
        <span>Page {page} of {totalPages}</span>
        <button
          onClick={() => setPage(p => p + 1)}
          disabled={page >= totalPages || isLoading}
        >
          Next
        </button>
      </div>
    </div>
  );
}

export default Users;

Product Search with Filters ​

typescript
// src/pages/Products.tsx
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import { gte, lte, inOp } from '@pubflow/flowfull-client';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  stock: number;
}

function Products() {
  const [products, setProducts] = useState<Product[]>([]);
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('');
  const [minPrice, setMinPrice] = useState('');
  const [maxPrice, setMaxPrice] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    fetchProducts();
  }, [search, category, minPrice, maxPrice]);

  async function fetchProducts() {
    setIsLoading(true);

    let query = api.query('/products');

    // Add search
    if (search) {
      query = query.search(search);
    }

    // Add category filter
    if (category) {
      query = query.where('category', category);
    }

    // Add price range
    if (minPrice) {
      query = query.where('price', gte(parseFloat(minPrice)));
    }
    if (maxPrice) {
      query = query.where('price', lte(parseFloat(maxPrice)));
    }

    // Only in stock
    query = query.where('stock', gte(1));

    // Sort and paginate
    query = query.sort('name', 'asc').limit(20);

    const response = await query.get<Product[]>();

    if (response.success) {
      setProducts(response.data || []);
    }

    setIsLoading(false);
  }

  return (
    <div>
      <h1>Products</h1>

      <div className="filters">
        <input
          type="text"
          placeholder="Search..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
        />

        <select value={category} onChange={(e) => setCategory(e.target.value)}>
          <option value="">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
          <option value="books">Books</option>
        </select>

        <input
          type="number"
          placeholder="Min Price"
          value={minPrice}
          onChange={(e) => setMinPrice(e.target.value)}
        />

        <input
          type="number"
          placeholder="Max Price"
          value={maxPrice}
          onChange={(e) => setMaxPrice(e.target.value)}
        />
      </div>

      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <div className="product-grid">
          {products.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p>${product.price}</p>
              <p>Category: {product.category}</p>
              <p>Stock: {product.stock}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

export default Products;

Create/Update Form ​

typescript
// src/pages/CreateUser.tsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { api } from '../api/client';

function CreateUser() {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    user_type: 'user'
  });
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsLoading(true);
    setError('');

    const response = await api.post('/users', formData);

    if (response.success) {
      navigate('/users');
    } else {
      setError(response.error || 'Failed to create user');
    }

    setIsLoading(false);
  }

  return (
    <div>
      <h1>Create User</h1>

      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="Name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          required
        />

        <input
          type="email"
          placeholder="Email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value.toLowerCase() })}
          required
        />

        <select
          value={formData.user_type}
          onChange={(e) => setFormData({ ...formData, user_type: e.target.value })}
        >
          <option value="user">User</option>
          <option value="admin">Admin</option>
        </select>

        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Creating...' : 'Create User'}
        </button>

        {error && <div className="error">{error}</div>}
      </form>
    </div>
  );
}

export default CreateUser;

Using with React Query (Optional) ​

For advanced caching and state management, you can use React Query:

bash
npm install @tanstack/react-query
typescript
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../api/client';

interface User {
  id: string;
  name: string;
  email: string;
}

export function useUsers(page = 1, limit = 10) {
  return useQuery({
    queryKey: ['users', page, limit],
    queryFn: async () => {
      const response = await api
        .query('/users')
        .page(page)
        .limit(limit)
        .get<User[]>();

      if (!response.success) {
        throw new Error(response.error);
      }

      return {
        users: response.data,
        meta: response.meta
      };
    }
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (data: Partial<User>) => {
      const response = await api.post<User>('/users', data);

      if (!response.success) {
        throw new Error(response.error);
      }

      return response.data;
    },
    onSuccess: () => {
      // Invalidate users query to refetch
      queryClient.invalidateQueries({ queryKey: ['users'] });
    }
  });
}

// Usage in component
function UserList() {
  const { data, isLoading, error } = useUsers(1, 10);
  const createUser = useCreateUser();

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

  return (
    <div>
      {data?.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Next Steps ​