Skip to the content.

React Integration

This guide shows how to integrate Firebase ORM with React applications, covering Create React App, Vite, and custom setups with hooks, context, and state management.

Quick Setup

1. Installation

npm install @arbel/firebase-orm firebase moment --save

For development tools:

npm install --save-dev @types/node

2. TypeScript Configuration

Update your tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  },
  "include": ["src"]
}

3. Environment Configuration

Create a .env file:

REACT_APP_FIREBASE_API_KEY=your-api-key
REACT_APP_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
REACT_APP_FIREBASE_PROJECT_ID=your-project-id
REACT_APP_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=123456789
REACT_APP_FIREBASE_APP_ID=your-app-id

Firebase Setup

Firebase Configuration

// src/config/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);

// Initialize Firebase ORM
FirestoreOrmRepository.initGlobalConnection(firestore);

export { firestore, app };

Initialize in App

// src/App.tsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { FirebaseProvider } from './context/FirebaseContext';
import { UserProvider } from './context/UserContext';
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
import CreateUser from './components/CreateUser';
import Navigation from './components/Navigation';
import './config/firebase'; // Initialize Firebase

import './App.css';

function App() {
  return (
    <FirebaseProvider>
      <UserProvider>
        <Router>
          <div className="min-h-screen bg-gray-50">
            <Navigation />
            <main className="container mx-auto px-4 py-8">
              <Routes>
                <Route path="/" element={<UserList />} />
                <Route path="/users" element={<UserList />} />
                <Route path="/users/:id" element={<UserDetail />} />
                <Route path="/create" element={<CreateUser />} />
              </Routes>
            </main>
          </div>
        </Router>
      </UserProvider>
    </FirebaseProvider>
  );
}

export default App;

Model Definition

// src/models/User.ts
import { BaseModel, Model, Field, HasMany } from '@arbel/firebase-orm';
import { Post } from './Post';

@Model({
  reference_path: 'users',
  path_id: 'user_id'
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: true })
  public email!: string;

  @Field({ field_name: 'created_at' })
  public createdAt?: string;

  @Field({ is_required: false })
  public bio?: string;

  @Field({ is_required: false })
  public avatar?: string;

  @HasMany({ model: Post, foreignKey: 'author_id' })
  public posts?: Post[];

  // React-specific helper methods
  toJSON() {
    return {
      id: this.getId(),
      name: this.name,
      email: this.email,
      createdAt: this.createdAt,
      bio: this.bio,
      avatar: this.avatar,
    };
  }

  static async findByEmail(email: string): Promise<User | null> {
    const users = await User.query().where('email', '==', email).get();
    return users.length > 0 ? users[0] : null;
  }

  getDisplayName(): string {
    return this.name || this.email;
  }

  getInitials(): string {
    return this.name
      ? this.name.split(' ').map(n => n[0]).join('').toUpperCase()
      : this.email[0].toUpperCase();
  }
}

Context and State Management

Firebase Context

// src/context/FirebaseContext.tsx
import React, { createContext, useContext, ReactNode } from 'react';
import { firestore } from '../config/firebase';
import { Firestore } from 'firebase/firestore';

interface FirebaseContextType {
  firestore: Firestore;
}

const FirebaseContext = createContext<FirebaseContextType | undefined>(undefined);

export function FirebaseProvider({ children }: { children: ReactNode }) {
  const value = {
    firestore,
  };

  return (
    <FirebaseContext.Provider value={value}>
      {children}
    </FirebaseContext.Provider>
  );
}

export function useFirebase() {
  const context = useContext(FirebaseContext);
  if (context === undefined) {
    throw new Error('useFirebase must be used within a FirebaseProvider');
  }
  return context;
}

User Context with State Management

// src/context/UserContext.tsx
import React, { createContext, useContext, useReducer, ReactNode, useEffect } from 'react';
import { User } from '../models/User';

interface UserState {
  users: User[];
  loading: boolean;
  error: string | null;
  selectedUser: User | null;
}

type UserAction =
  | { type: 'SET_LOADING'; payload: boolean }
  | { type: 'SET_ERROR'; payload: string | null }
  | { type: 'SET_USERS'; payload: User[] }
  | { type: 'ADD_USER'; payload: User }
  | { type: 'UPDATE_USER'; payload: User }
  | { type: 'DELETE_USER'; payload: string }
  | { type: 'SET_SELECTED_USER'; payload: User | null };

const initialState: UserState = {
  users: [],
  loading: false,
  error: null,
  selectedUser: null,
};

function userReducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    case 'SET_ERROR':
      return { ...state, error: action.payload, loading: false };
    case 'SET_USERS':
      return { ...state, users: action.payload, loading: false, error: null };
    case 'ADD_USER':
      return { ...state, users: [...state.users, action.payload] };
    case 'UPDATE_USER':
      return {
        ...state,
        users: state.users.map(user =>
          user.getId() === action.payload.getId() ? action.payload : user
        ),
        selectedUser: state.selectedUser?.getId() === action.payload.getId() 
          ? action.payload 
          : state.selectedUser
      };
    case 'DELETE_USER':
      return {
        ...state,
        users: state.users.filter(user => user.getId() !== action.payload),
        selectedUser: state.selectedUser?.getId() === action.payload 
          ? null 
          : state.selectedUser
      };
    case 'SET_SELECTED_USER':
      return { ...state, selectedUser: action.payload };
    default:
      return state;
  }
}

interface UserContextType extends UserState {
  loadUsers: () => Promise<void>;
  createUser: (userData: Partial<User>) => Promise<User>;
  updateUser: (id: string, updates: Partial<User>) => Promise<User>;
  deleteUser: (id: string) => Promise<void>;
  loadUser: (id: string) => Promise<User>;
  clearError: () => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

export function UserProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(userReducer, initialState);

  // Load all users
  const loadUsers = async () => {
    dispatch({ type: 'SET_LOADING', payload: true });
    dispatch({ type: 'SET_ERROR', payload: null });

    try {
      const users = await User.getAll();
      dispatch({ type: 'SET_USERS', payload: users });
    } catch (error) {
      dispatch({ 
        type: 'SET_ERROR', 
        payload: error instanceof Error ? error.message : 'Failed to load users' 
      });
    }
  };

  // Create user
  const createUser = async (userData: Partial<User>): Promise<User> => {
    try {
      const user = new User();
      user.initFromData(userData);
      user.createdAt = new Date().toISOString();
      await user.save();
      
      dispatch({ type: 'ADD_USER', payload: user });
      return user;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to create user';
      dispatch({ type: 'SET_ERROR', payload: errorMessage });
      throw error;
    }
  };

  // Update user
  const updateUser = async (id: string, updates: Partial<User>): Promise<User> => {
    try {
      const user = new User();
      await user.load(id);
      user.initFromData(updates);
      await user.save();
      
      dispatch({ type: 'UPDATE_USER', payload: user });
      return user;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to update user';
      dispatch({ type: 'SET_ERROR', payload: errorMessage });
      throw error;
    }
  };

  // Delete user
  const deleteUser = async (id: string): Promise<void> => {
    try {
      const user = new User();
      await user.load(id);
      await user.destroy();
      
      dispatch({ type: 'DELETE_USER', payload: id });
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to delete user';
      dispatch({ type: 'SET_ERROR', payload: errorMessage });
      throw error;
    }
  };

  // Load single user
  const loadUser = async (id: string): Promise<User> => {
    try {
      const user = new User();
      await user.load(id);
      dispatch({ type: 'SET_SELECTED_USER', payload: user });
      return user;
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Failed to load user';
      dispatch({ type: 'SET_ERROR', payload: errorMessage });
      throw error;
    }
  };

  // Clear error
  const clearError = () => {
    dispatch({ type: 'SET_ERROR', payload: null });
  };

  // Set up real-time subscription
  useEffect(() => {
    const unsubscribe = User.onList((user: User) => {
      dispatch({ type: 'UPDATE_USER', payload: user });
    });

    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, []);

  const value: UserContextType = {
    ...state,
    loadUsers,
    createUser,
    updateUser,
    deleteUser,
    loadUser,
    clearError,
  };

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

export function useUsers() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUsers must be used within a UserProvider');
  }
  return context;
}

Custom Hooks

useFirebaseORM Hook

// src/hooks/useFirebaseORM.ts
import { useState, useEffect, useCallback } from 'react';

export function useFirebaseORM<T extends any>(
  ModelClass: new () => T,
  options: {
    realtime?: boolean;
    autoLoad?: boolean;
  } = {}
) {
  const [data, setData] = useState<T[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Load data
  const loadData = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const items = await (ModelClass as any).getAll();
      setData(items);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  }, [ModelClass]);

  // Create item
  const createItem = useCallback(async (itemData: Partial<T>) => {
    try {
      const item = new ModelClass();
      item.initFromData(itemData);
      await (item as any).save();

      if (!options.realtime) {
        setData(prev => [...prev, item]);
      }

      return item;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create item');
      throw err;
    }
  }, [ModelClass, options.realtime]);

  // Update item
  const updateItem = useCallback(async (id: string, updates: Partial<T>) => {
    try {
      const item = new ModelClass();
      await (item as any).load(id);
      item.initFromData(updates);
      await (item as any).save();

      if (!options.realtime) {
        setData(prev => prev.map(d => (d as any).getId() === id ? item : d));
      }

      return item;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to update item');
      throw err;
    }
  }, [ModelClass, options.realtime]);

  // Delete item
  const deleteItem = useCallback(async (id: string) => {
    try {
      const item = new ModelClass();
      await (item as any).load(id);
      await (item as any).destroy();

      if (!options.realtime) {
        setData(prev => prev.filter(d => (d as any).getId() !== id));
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to delete item');
      throw err;
    }
  }, [ModelClass, options.realtime]);

  // Auto-load data on mount
  useEffect(() => {
    if (options.autoLoad !== false) {
      loadData();
    }
  }, [loadData, options.autoLoad]);

  // Set up real-time subscription
  useEffect(() => {
    if (options.realtime) {
      const unsubscribe = (ModelClass as any).onList((item: T) => {
        setData(currentData => {
          const index = currentData.findIndex(d => (d as any).getId() === (item as any).getId());
          if (index >= 0) {
            const newData = [...currentData];
            newData[index] = item;
            return newData;
          }
          return [...currentData, item];
        });
      });

      return unsubscribe;
    }
  }, [ModelClass, options.realtime]);

  return {
    data,
    loading,
    error,
    loadData,
    createItem,
    updateItem,
    deleteItem,
    setError,
  };
}

useUser Hook

// src/hooks/useUser.ts
import { useState, useEffect } from 'react';
import { User } from '../models/User';

export function useUser(userId?: string) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!userId) {
      setUser(null);
      return;
    }

    const loadUser = async () => {
      setLoading(true);
      setError(null);

      try {
        const userInstance = new User();
        await userInstance.load(userId);
        setUser(userInstance);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to load user');
      } finally {
        setLoading(false);
      }
    };

    loadUser();
  }, [userId]);

  const loadUserPosts = async () => {
    if (!user) return [];

    try {
      const posts = await user.loadHasMany('posts');
      // Update user with loaded posts
      setUser({ ...user, posts });
      return posts;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to load user posts');
      return [];
    }
  };

  return {
    user,
    loading,
    error,
    loadUserPosts,
  };
}

Components

User List Component

// src/components/UserList.tsx
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useUsers } from '../context/UserContext';
import UserCard from './UserCard';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';

const UserList: React.FC = () => {
  const { users, loading, error, loadUsers, deleteUser, clearError } = useUsers();

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

  const handleDeleteUser = async (userId: string) => {
    if (window.confirm('Are you sure you want to delete this user?')) {
      try {
        await deleteUser(userId);
      } catch (error) {
        // Error is handled by context
        console.error('Failed to delete user:', error);
      }
    }
  };

  if (loading && users.length === 0) {
    return <LoadingSpinner />;
  }

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold text-gray-900">Users</h1>
        <Link
          to="/create"
          className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors"
        >
          Add User
        </Link>
      </div>

      {error && (
        <ErrorMessage 
          message={error} 
          onDismiss={clearError}
        />
      )}

      {users.length === 0 && !loading ? (
        <div className="text-center py-12">
          <p className="text-gray-500 text-lg">No users found</p>
          <Link
            to="/create"
            className="text-blue-500 hover:text-blue-600 mt-2 inline-block"
          >
            Create your first user
          </Link>
        </div>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {users.map(user => (
            <UserCard
              key={user.getId()}
              user={user}
              onDelete={handleDeleteUser}
            />
          ))}
        </div>
      )}

      {loading && users.length > 0 && (
        <div className="text-center py-4">
          <LoadingSpinner size="sm" />
        </div>
      )}
    </div>
  );
};

export default UserList;

User Card Component

// src/components/UserCard.tsx
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { User } from '../models/User';

interface UserCardProps {
  user: User;
  onDelete: (userId: string) => void;
}

const UserCard: React.FC<UserCardProps> = ({ user, onDelete }) => {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const [posts, setPosts] = useState<any[]>([]);
  const [loadingPosts, setLoadingPosts] = useState(false);

  const handleLoadPosts = async () => {
    if (loadingPosts) return;

    setLoadingPosts(true);
    try {
      const userPosts = await user.loadHasMany('posts');
      setPosts(userPosts);
    } catch (error) {
      console.error('Error loading posts:', error);
    } finally {
      setLoadingPosts(false);
    }
  };

  const formatDate = (dateString?: string) => {
    if (!dateString) return 'N/A';
    return new Date(dateString).toLocaleDateString();
  };

  return (
    <div className="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden">
      <div className="p-6">
        <div className="flex items-start justify-between">
          <div className="flex items-center space-x-3">
            <div className="w-12 h-12 bg-gray-300 rounded-full flex items-center justify-center">
              {user.avatar ? (
                <img
                  src={user.avatar}
                  alt={user.name}
                  className="w-12 h-12 rounded-full object-cover"
                />
              ) : (
                <span className="text-gray-600 font-medium">
                  {user.getInitials()}
                </span>
              )}
            </div>
            <div>
              <h3 className="text-lg font-semibold text-gray-900">
                {user.getDisplayName()}
              </h3>
              <p className="text-sm text-gray-600">{user.email}</p>
            </div>
          </div>

          <div className="relative">
            <button
              onClick={() => setIsMenuOpen(!isMenuOpen)}
              className="text-gray-400 hover:text-gray-600"
            >
              <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                <path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
              </svg>
            </button>

            {isMenuOpen && (
              <div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 border border-gray-200">
                <div className="py-1">
                  <Link
                    to={`/users/${user.getId()}`}
                    className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                  >
                    View Details
                  </Link>
                  <button
                    onClick={handleLoadPosts}
                    disabled={loadingPosts}
                    className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 disabled:opacity-50"
                  >
                    {loadingPosts ? 'Loading Posts...' : 'Load Posts'}
                  </button>
                  <button
                    onClick={() => {
                      onDelete(user.getId());
                      setIsMenuOpen(false);
                    }}
                    className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100"
                  >
                    Delete
                  </button>
                </div>
              </div>
            )}
          </div>
        </div>

        {user.bio && (
          <p className="mt-3 text-sm text-gray-600">{user.bio}</p>
        )}

        <div className="mt-4 text-xs text-gray-500">
          Created: {formatDate(user.createdAt)}
        </div>

        {posts.length > 0 && (
          <div className="mt-4 pt-4 border-t border-gray-200">
            <h4 className="text-sm font-medium text-gray-900 mb-2">
              Posts ({posts.length})
            </h4>
            <div className="space-y-1">
              {posts.slice(0, 3).map((post, index) => (
                <div key={index} className="text-xs text-gray-600 p-2 bg-gray-50 rounded">
                  {post.title}
                </div>
              ))}
              {posts.length > 3 && (
                <div className="text-xs text-gray-500">
                  +{posts.length - 3} more posts
                </div>
              )}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default UserCard;

User Detail Component

// src/components/UserDetail.tsx
import React from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useUser } from '../hooks/useUser';
import LoadingSpinner from './LoadingSpinner';
import ErrorMessage from './ErrorMessage';

const UserDetail: React.FC = () => {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const { user, loading, error, loadUserPosts } = useUser(id);

  const handleLoadPosts = async () => {
    try {
      await loadUserPosts();
    } catch (error) {
      console.error('Failed to load posts:', error);
    }
  };

  const formatDate = (dateString?: string) => {
    if (!dateString) return 'N/A';
    return new Date(dateString).toLocaleDateString();
  };

  if (loading) {
    return <LoadingSpinner />;
  }

  if (error) {
    return (
      <ErrorMessage 
        message={error}
        onDismiss={() => navigate('/users')}
      />
    );
  }

  if (!user) {
    return (
      <div className="text-center py-12">
        <p className="text-gray-500 text-lg">User not found</p>
        <Link
          to="/users"
          className="text-blue-500 hover:text-blue-600 mt-2 inline-block"
        >
          Back to Users
        </Link>
      </div>
    );
  }

  return (
    <div className="max-w-4xl mx-auto">
      <div className="mb-6">
        <Link
          to="/users"
          className="text-blue-500 hover:text-blue-600 flex items-center"
        >
          <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
          </svg>
          Back to Users
        </Link>
      </div>

      <div className="bg-white rounded-lg shadow-lg overflow-hidden">
        <div className="p-8">
          <div className="flex items-center space-x-6 mb-6">
            <div className="w-24 h-24 bg-gray-300 rounded-full flex items-center justify-center">
              {user.avatar ? (
                <img
                  src={user.avatar}
                  alt={user.name}
                  className="w-24 h-24 rounded-full object-cover"
                />
              ) : (
                <span className="text-gray-600 font-medium text-2xl">
                  {user.getInitials()}
                </span>
              )}
            </div>
            <div>
              <h1 className="text-3xl font-bold text-gray-900">
                {user.getDisplayName()}
              </h1>
              <p className="text-lg text-gray-600">{user.email}</p>
              <p className="text-sm text-gray-500 mt-1">
                Member since {formatDate(user.createdAt)}
              </p>
            </div>
          </div>

          {user.bio && (
            <div className="mb-6">
              <h2 className="text-xl font-semibold text-gray-900 mb-2">Bio</h2>
              <p className="text-gray-700">{user.bio}</p>
            </div>
          )}

          <div className="border-t pt-6">
            <div className="flex justify-between items-center mb-4">
              <h2 className="text-xl font-semibold text-gray-900">Posts</h2>
              <button
                onClick={handleLoadPosts}
                className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors"
              >
                Load Posts
              </button>
            </div>

            {user.posts ? (
              user.posts.length > 0 ? (
                <div className="grid gap-4">
                  {user.posts.map((post, index) => (
                    <div key={index} className="p-4 border border-gray-200 rounded-md">
                      <h3 className="font-medium text-gray-900">{post.title}</h3>
                      <p className="text-gray-600 text-sm mt-1">{post.content}</p>
                      <p className="text-gray-400 text-xs mt-2">
                        {formatDate(post.createdAt)}
                      </p>
                    </div>
                  ))}
                </div>
              ) : (
                <p className="text-gray-500">No posts found</p>
              )
            ) : (
              <p className="text-gray-500">Click "Load Posts" to view user posts</p>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default UserDetail;

Create User Form

// src/components/CreateUser.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUsers } from '../context/UserContext';
import ErrorMessage from './ErrorMessage';

const CreateUser: React.FC = () => {
  const navigate = useNavigate();
  const { createUser, loading } = useUsers();
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    bio: '',
    avatar: ''
  });
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);

    if (!formData.name.trim() || !formData.email.trim()) {
      setError('Name and email are required');
      return;
    }

    try {
      await createUser(formData);
      navigate('/users');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create user');
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

  return (
    <div className="max-w-2xl mx-auto">
      <h1 className="text-3xl font-bold text-gray-900 mb-8">Create New User</h1>

      {error && (
        <ErrorMessage 
          message={error}
          onDismiss={() => setError(null)}
        />
      )}

      <form onSubmit={handleSubmit} className="bg-white shadow-lg rounded-lg p-8">
        <div className="space-y-6">
          <div>
            <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
              Name *
            </label>
            <input
              type="text"
              id="name"
              name="name"
              value={formData.name}
              onChange={handleChange}
              required
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="Enter full name"
            />
          </div>

          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
              Email *
            </label>
            <input
              type="email"
              id="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              required
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="Enter email address"
            />
          </div>

          <div>
            <label htmlFor="avatar" className="block text-sm font-medium text-gray-700 mb-1">
              Avatar URL
            </label>
            <input
              type="url"
              id="avatar"
              name="avatar"
              value={formData.avatar}
              onChange={handleChange}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="https://example.com/avatar.jpg"
            />
          </div>

          <div>
            <label htmlFor="bio" className="block text-sm font-medium text-gray-700 mb-1">
              Bio
            </label>
            <textarea
              id="bio"
              name="bio"
              value={formData.bio}
              onChange={handleChange}
              rows={4}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="Tell us about yourself..."
            />
          </div>
        </div>

        <div className="flex justify-end space-x-4 mt-8">
          <button
            type="button"
            onClick={() => navigate('/users')}
            className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
          >
            Cancel
          </button>
          <button
            type="submit"
            disabled={loading}
            className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          >
            {loading ? 'Creating...' : 'Create User'}
          </button>
        </div>
      </form>
    </div>
  );
};

export default CreateUser;

Utility Components

Loading Spinner

// src/components/LoadingSpinner.tsx
import React from 'react';

interface LoadingSpinnerProps {
  size?: 'sm' | 'md' | 'lg';
}

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ size = 'md' }) => {
  const sizeClasses = {
    sm: 'h-4 w-4',
    md: 'h-8 w-8',
    lg: 'h-12 w-12'
  };

  return (
    <div className="flex justify-center items-center py-8">
      <div
        className={`animate-spin rounded-full border-b-2 border-blue-500 ${sizeClasses[size]}`}
      />
    </div>
  );
};

export default LoadingSpinner;

Error Message

// src/components/ErrorMessage.tsx
import React from 'react';

interface ErrorMessageProps {
  message: string;
  onDismiss?: () => void;
}

const ErrorMessage: React.FC<ErrorMessageProps> = ({ message, onDismiss }) => {
  return (
    <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
      <div className="flex">
        <div className="flex-shrink-0">
          <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
            <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
          </svg>
        </div>
        <div className="ml-3">
          <p className="text-sm text-red-800">{message}</p>
        </div>
        {onDismiss && (
          <div className="ml-auto pl-3">
            <div className="-mx-1.5 -my-1.5">
              <button
                onClick={onDismiss}
                className="inline-flex rounded-md p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600"
              >
                <span className="sr-only">Dismiss</span>
                <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                  <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
                </svg>
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default ErrorMessage;

State Management with Redux Toolkit

If you prefer Redux Toolkit for state management:

// src/store/userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { User } from '../models/User';

interface UserState {
  users: User[];
  selectedUser: User | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  users: [],
  selectedUser: null,
  loading: false,
  error: null,
};

// Async thunks
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
  const users = await User.getAll();
  return users;
});

export const createUser = createAsyncThunk(
  'users/createUser',
  async (userData: Partial<User>) => {
    const user = new User();
    user.initFromData(userData);
    user.createdAt = new Date().toISOString();
    await user.save();
    return user;
  }
);

export const updateUser = createAsyncThunk(
  'users/updateUser',
  async ({ id, updates }: { id: string; updates: Partial<User> }) => {
    const user = new User();
    await user.load(id);
    user.initFromData(updates);
    await user.save();
    return user;
  }
);

export const deleteUser = createAsyncThunk('users/deleteUser', async (id: string) => {
  const user = new User();
  await user.load(id);
  await user.destroy();
  return id;
});

const userSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    clearError: (state) => {
      state.error = null;
    },
    setSelectedUser: (state, action) => {
      state.selectedUser = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      // Fetch users
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch users';
      })
      // Create user
      .addCase(createUser.fulfilled, (state, action) => {
        state.users.push(action.payload);
      })
      // Update user
      .addCase(updateUser.fulfilled, (state, action) => {
        const index = state.users.findIndex(user => user.getId() === action.payload.getId());
        if (index >= 0) {
          state.users[index] = action.payload;
        }
      })
      // Delete user
      .addCase(deleteUser.fulfilled, (state, action) => {
        state.users = state.users.filter(user => user.getId() !== action.payload);
      });
  },
});

export const { clearError, setSelectedUser } = userSlice.actions;
export default userSlice.reducer;

Testing

Jest Setup

// src/setupTests.ts
import '@testing-library/jest-dom';

// Mock Firebase
jest.mock('./config/firebase', () => ({
  firestore: {},
  app: {},
}));

// Mock Firebase ORM
jest.mock('@arbel/firebase-orm', () => ({
  FirestoreOrmRepository: {
    initGlobalConnection: jest.fn(),
  },
  BaseModel: class MockBaseModel {
    save = jest.fn();
    load = jest.fn();
    destroy = jest.fn();
    getId = jest.fn(() => 'mock-id');
    static getAll = jest.fn(() => Promise.resolve([]));
    static query = jest.fn(() => ({
      where: jest.fn().mockReturnThis(),
      get: jest.fn(() => Promise.resolve([])),
    }));
    static onList = jest.fn(() => jest.fn());
  },
  Model: () => (target: any) => target,
  Field: () => (target: any, propertyKey: string) => {},
  HasMany: () => (target: any, propertyKey: string) => {},
}));

Component Tests

// src/components/__tests__/UserCard.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import UserCard from '../UserCard';
import { User } from '../../models/User';

// Mock user
const mockUser = {
  getId: () => 'test-id',
  name: 'John Doe',
  email: 'john@example.com',
  getDisplayName: () => 'John Doe',
  getInitials: () => 'JD',
  loadHasMany: jest.fn(() => Promise.resolve([])),
} as unknown as User;

const renderWithRouter = (component: React.ReactElement) => {
  return render(
    <BrowserRouter>
      {component}
    </BrowserRouter>
  );
};

describe('UserCard', () => {
  it('renders user information correctly', () => {
    const mockOnDelete = jest.fn();
    
    renderWithRouter(
      <UserCard user={mockUser} onDelete={mockOnDelete} />
    );

    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
    expect(screen.getByText('JD')).toBeInTheDocument();
  });

  it('calls onDelete when delete button is clicked', () => {
    const mockOnDelete = jest.fn();
    
    renderWithRouter(
      <UserCard user={mockUser} onDelete={mockOnDelete} />
    );

    // Open menu
    fireEvent.click(screen.getByRole('button'));
    
    // Click delete
    fireEvent.click(screen.getByText('Delete'));

    expect(mockOnDelete).toHaveBeenCalledWith('test-id');
  });
});

Best Practices

1. Error Boundaries

// src/components/ErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-4 bg-red-50 border border-red-200 rounded-md">
          <h3 className="text-red-800 font-medium">Something went wrong</h3>
          <p className="text-red-600 text-sm mt-1">
            {this.state.error?.message || 'An unexpected error occurred'}
          </p>
        </div>
      );
    }

    return this.props.children;
  }
}

2. Performance Optimization

// Use React.memo for expensive components
const UserCard = React.memo<UserCardProps>(({ user, onDelete }) => {
  // Component implementation
}, (prevProps, nextProps) => {
  // Custom comparison
  return prevProps.user.getId() === nextProps.user.getId();
});

// Use useMemo for expensive calculations
const expensiveValue = useMemo(() => {
  return users.filter(user => user.isActive).length;
}, [users]);

// Use useCallback for event handlers
const handleUserSelect = useCallback((userId: string) => {
  setSelectedUser(userId);
}, []);

3. Code Splitting

// Lazy load components
const UserDetail = React.lazy(() => import('./components/UserDetail'));
const CreateUser = React.lazy(() => import('./components/CreateUser'));

// Use Suspense
<Suspense fallback={<LoadingSpinner />}>
  <Routes>
    <Route path="/users/:id" element={<UserDetail />} />
    <Route path="/create" element={<CreateUser />} />
  </Routes>
</Suspense>

Common Issues

1. Decorator Support

Ensure TypeScript configuration includes experimental decorators.

2. Real-time Subscriptions

Always clean up subscriptions in useEffect cleanup function.

3. State Management

Choose between Context API for simple state and Redux for complex applications.

Next Steps