Skip to content

React app slice

A complete React app slice with provider, routing, query, mutation, and action.

Entry point

tsx
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Client } from '@pbvex/client';
import { PBVexProvider } from '@pbvex/react';
import { App } from './App.js';

const client = new Client('http://localhost:8090');

createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <PBVexProvider client={client}>
      <App />
    </PBVexProvider>
  </React.StrictMode>,
);

App

tsx
// App.tsx
import { Login } from './Login.js';
import { MessageView } from './MessageView.js';
import { useEffect, useState } from 'react';
import { usePBVexClient } from '@pbvex/react';

export function App() {
  const client = usePBVexClient();
  const [userId, setUserId] = useState<string | null>(
    client.authStore.isValid ? client.authStore.record?.id ?? null : null,
  );

  useEffect(() => {
    const syncAuth = () => setUserId(
      client.authStore.isValid ? client.authStore.record?.id ?? null : null,
    );
    const unsubscribe = client.authStore.onChange(syncAuth);
    const timer = window.setInterval(syncAuth, 30_000);
    return () => {
      unsubscribe();
      window.clearInterval(timer);
    };
  }, [client]);

  return (
    <div>
      <h1>PBVex Messages</h1>
      {userId ? (
        <MessageView channel="general" />
      ) : (
        <Login onLogin={setUserId} />
      )}
    </div>
  );
}

Login

tsx
// Login.tsx
import { usePBVexClient } from '@pbvex/react';
import { useState } from 'react';

export function Login({ onLogin }: { onLogin: (userId: string) => void }) {
  const client = usePBVexClient();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const result = await client.auth
      .collection('users')
      .authWithPassword(email, password);
    onLogin(result.record.id);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Log in</button>
    </form>
  );
}

Message list

tsx
// MessageView.tsx
import { useQuery, useMutation, useAction } from '@pbvex/react';
import { useState } from 'react';
import { api } from './pbvex/_generated/api.js';

interface Message {
  id: string;
  body: string;
  channel: string;
}

export function MessageView({ channel }: { channel: string }) {
  const messages = useQuery(api.messages.list, { channel });
  const send = useMutation(api.messages.send);
  const notify = useAction(api.messages.notify);
  const [body, setBody] = useState('');

  if (messages === undefined) return <p>Loading…</p>;

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const sent = await send({ body, channel });
    setBody('');
    await notify({ messageId: sent.id });
  }

  return (
    <div>
      <ul>
        {messages.map((message: Message) => (
          <li key={message.id}>{message.body}</li>
        ))}
      </ul>
      <form onSubmit={handleSubmit}>
        <input value={body} onChange={(e) => setBody(e.target.value)} />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Error boundary

tsx
// ErrorFallback.tsx
export function ErrorFallback({ error }: { error: Error }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
    </div>
  );
}

Wrap MessageView with an error boundary to catch PBVexError thrown by useQuery.

Generated API reference. Source of truth is the codebase.