Skip to content

dharayush7/fireclass-react

Repository files navigation

Fireclass logo

@dharayush7/fireclass-react

Type-safe Firestore models and realtime React hooks using the modular Firebase client SDK.

Setup | API reference | Realtime guide | GitHub | npm

@dharayush7/fireclass-react binds Fireclass models to the Firebase client SDK and adds live collection and document hooks powered by onSnapshot.

Client security boundary: Firebase web configuration identifies your application but is not authorization. Production access must be enforced with Firebase Authentication, Firestore Security Rules, and App Check where appropriate.

Installation

npm install @dharayush7/fireclass-react firebase react class-validator class-transformer reflect-metadata

Enable decorators in Vite's application configuration or the TypeScript config that compiles source:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Firebase client setup

Register a Firebase Web app and add its public values to .env.local:

VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_APP_ID=your-app-id

Create the Firebase client entry:

// src/lib/firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);

Bind Fireclass once:

// src/lib/fireclass.ts
import "reflect-metadata";
import { createFireclass } from "@dharayush7/fireclass-react";
import { db } from "./firebase";

export const {
  BaseModel,
  adapter,
  useQuery,
  useDoc,
} = createFireclass(db);

Use one shared binding so every model and hook communicates with the same Firebase app. Keep the local entry limited to initialized values; import decorators and errors directly from the SDK.

Define a model

// src/models/todo.ts
import { Collection } from "@dharayush7/fireclass-react";
import { IsBoolean, IsDate, IsString, Length } from "class-validator";
import { Type } from "class-transformer";
import { BaseModel } from "../lib/fireclass";

@Collection("todos")
export class Todo extends BaseModel<Todo> {
  @IsString()
  @Length(1, 120)
  title!: string;

  @IsBoolean()
  done!: boolean;

  @IsDate()
  @Type(() => Date)
  createdAt!: Date;

  constructor(data?: Partial<Todo>) {
    super(data);
    Object.assign(this, data);
  }
}

Import reflect-metadata before models load, normally in src/main.tsx.

Realtime query

import { ValidationFailedError } from "@dharayush7/fireclass-react";
import { useState } from "react";
import { useQuery } from "./lib/fireclass";
import { Todo } from "./models/todo";

export function TodoList() {
  const { data: todos, loading, error } = useQuery(Todo, {
    where: { done: { equals: false } },
    orderBy: { createdAt: "desc" },
    limit: 100,
  });

  const [title, setTitle] = useState("");

  async function addTodo() {
    try {
      await new Todo({
        title: title.trim(),
        done: false,
        createdAt: new Date(),
      }).save();
      setTitle("");
    } catch (error) {
      if (error instanceof ValidationFailedError) {
        console.error(error.errors);
      }
    }
  }

  if (loading) return <p>Loading...</p>;
  if (error) return <p role="alert">{error.message}</p>;

  return (
    <section>
      <button type="button" onClick={() => void addTodo()}>
        Add todo
      </button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => {
                  todo.done = !todo.done;
                  void todo.save();
                }}
              />
              {todo.title}
            </label>
          </li>
        ))}
      </ul>
    </section>
  );
}

Writes use the model API and active hooks receive the resulting snapshot without a manual refetch.

Subscribe to one document

import { useDoc } from "./lib/fireclass";
import { Todo } from "./models/todo";

function TodoDetails({ id }: { id?: string }) {
  const { data: todo, loading, error } = useDoc(Todo, id);

  if (loading) return <p>Loading...</p>;
  if (error) return <p role="alert">{error.message}</p>;
  if (!todo) return <p>Todo not found.</p>;

  return <h2>{todo.title}</h2>;
}

Passing undefined skips the subscription and returns a settled null state.

Hook state behavior

useQuery

function useQuery<T>(
  model: ModelCtor<T>,
  options?: QueryOptions<T>,
): {
  data: T[];
  loading: boolean;
  error: Error | null;
};
Event State
Initial render or resubscribe Previous data, loading true, error null
Snapshot Hydrated data, loading false, error null
Subscription error Empty data, loading false, received error
Unmount or dependency change Active listener unsubscribes

Equivalent inline query objects are normalized to avoid unnecessary subscriptions.

Realtime query stabilization currently uses JSON serialization. Use JSON-native filter and cursor values. Date instances, document references, and DocumentSnapshots do not retain runtime identity through this hook.

useDoc

function useDoc<T>(
  model: ModelCtor<T>,
  id: string | undefined,
): {
  data: T | null;
  loading: boolean;
  error: Error | null;
};

A missing document resolves to null. Subscription errors clear data and expose the Firebase error. Listeners are removed when the id or model changes and when the component unmounts.

Export index

Export Purpose
createFireclass(db) Return BaseModel, ClientAdapter, useQuery, and useDoc
Fireclass Return type of createFireclass
ClientAdapter Firebase client implementation of CRUD, queries, counts, and realtime
RealtimeAdapter Core adapter plus collection and document subscriptions
makeHooks(adapter) Build hooks from a custom realtime adapter
ModelLike, ModelCtor Minimum model structure accepted by hooks
QueryResult, DocResult Hook state interfaces
Core exports Models, decorators, query types, validation, conversion, and errors

ClientAdapter capabilities

Method Firebase client operation
add addDoc
set setDoc with merge
get getDoc
query getDocs
delete deleteDoc
batchDelete Write batches of at most 500 deletes
count getCountFromServer
subscribe Query onSnapshot
subscribeDoc Document onSnapshot
convert Recursive Timestamp-to-Date conversion

CLI setup

Create src/lib/firebase.ts before running the initializer. The CLI references existing Firebase files and does not overwrite them.

npx fireclass init
npx fireclass doctor
npm run build

Choose React, the db export, and the application TypeScript config. The CLI writes fireclass.json, the Fireclass binding, a starter model, and decorator options.

Documentation and examples

See CHANGELOG.md for version history and RELEASE_NOTES.md for the current release summary.

License

MIT. Copyright Ayush Dhar.