or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdauth.mddatabase.mdindex.mdnextjs.mdreact.mdschema.mdserver-functions.mdvalues-validators.md
tile.json

react.mddocs/

React Integration

Setup

import { ConvexProvider, ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);

function App() {
  return (
    <ConvexProvider client={convex}>
      <YourApp />
    </ConvexProvider>
  );
}

Hooks

useQuery - Load Reactive Data

import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function Messages() {
  const messages = useQuery(api.messages.list);

  if (messages === undefined) return <div>Loading...</div>;

  return (
    <div>
      {messages.map(m => <p key={m._id}>{m.body}</p>)}
    </div>
  );
}

// With arguments
function UserMessages({ userId }: { userId: string }) {
  const messages = useQuery(api.messages.getByUser, { userId });
  // ...
}

// Skip conditionally
function ConditionalData({ shouldLoad }: { shouldLoad: boolean }) {
  const data = useQuery(api.data.get, shouldLoad ? {} : 'skip');
  // Returns undefined when skipped
}

useMutation - Execute Mutations

import { useMutation } from 'convex/react';

function SendMessage() {
  const send = useMutation(api.messages.send);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await send({ author: 'Alice', body: 'Hello!' });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Send</button>
    </form>
  );
}

// With error handling
function CreatePost() {
  const create = useMutation(api.posts.create);
  const [error, setError] = useState<string | null>(null);

  const handleCreate = async (title: string, body: string) => {
    try {
      await create({ title, body });
      setError(null);
    } catch (err) {
      setError(err.message);
    }
  };

  return error ? <div>Error: {error}</div> : <button onClick={...}>Create</button>;
}

Optimistic Updates

import { useMutation } from 'convex/react';

function LikeButton({ postId }: { postId: string }) {
  const like = useMutation(api.posts.like)
    .withOptimisticUpdate((store, args) => {
      const posts = store.getQuery(api.posts.list);
      if (posts) {
        store.setQuery(api.posts.list, {}, posts.map(p =>
          p._id === args.postId ? { ...p, likes: p.likes + 1 } : p
        ));
      }
    });

  return <button onClick={() => like({ postId })}>Like</button>;
}

// With paginated queries
import { usePaginatedQuery, optimisticallyUpdateValueInPaginatedQuery } from 'convex/react';

function MessageList() {
  const { results, loadMore, status } = usePaginatedQuery(
    api.messages.list,
    {},
    { initialNumItems: 20 }
  );

  const like = useMutation(api.messages.like)
    .withOptimisticUpdate((store, args) => {
      optimisticallyUpdateValueInPaginatedQuery(
        store,
        api.messages.list,
        {},
        msg => msg._id === args.id ? { ...msg, likes: msg.likes + 1 } : msg
      );
    });

  return (
    <div>
      {results.map(m => (
        <div key={m._id}>
          {m.body} ({m.likes} likes)
          <button onClick={() => like({ id: m._id })}>Like</button>
        </div>
      ))}
    </div>
  );
}

useAction - Execute Actions

import { useAction } from 'convex/react';

function SendEmail() {
  const sendEmail = useAction(api.emails.send);
  const [loading, setLoading] = useState(false);

  const handleSend = async () => {
    setLoading(true);
    try {
      await sendEmail({ to: 'user@example.com', subject: 'Hi', body: 'Hello!' });
    } finally {
      setLoading(false);
    }
  };

  return <button onClick={handleSend} disabled={loading}>Send</button>;
}

usePaginatedQuery - Infinite Scroll

import { usePaginatedQuery } from 'convex/react';

function InfiniteList() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.items.list,
    {},
    { initialNumItems: 20 }
  );

  return (
    <div>
      {results.map(item => <div key={item._id}>{item.name}</div>)}

      {status === 'CanLoadMore' && (
        <button onClick={() => loadMore(20)}>Load More</button>
      )}
      {status === 'LoadingMore' && <div>Loading...</div>}
      {status === 'Exhausted' && <div>No more items</div>}
    </div>
  );
}

// With intersection observer
function AutoLoadList() {
  const { results, status, loadMore } = usePaginatedQuery(
    api.items.list,
    {},
    { initialNumItems: 20 }
  );

  const loadMoreRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && status === 'CanLoadMore') {
          loadMore(20);
        }
      },
      { threshold: 1.0 }
    );

    if (loadMoreRef.current) observer.observe(loadMoreRef.current);
    return () => observer.disconnect();
  }, [status, loadMore]);

  return (
    <div>
      {results.map(item => <div key={item._id}>{item.name}</div>)}
      {status !== 'Exhausted' && <div ref={loadMoreRef}>Loading...</div>}
    </div>
  );
}

useConvexAuth - Auth State

import { useConvexAuth, Authenticated, Unauthenticated } from 'convex/react';

function App() {
  const { isLoading, isAuthenticated } = useConvexAuth();

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

  return (
    <div>
      <Authenticated>
        <Dashboard />
      </Authenticated>
      <Unauthenticated>
        <Login />
      </Unauthenticated>
    </div>
  );
}

useConvex - Access Client

import { useConvex } from 'convex/react';

function Component() {
  const convex = useConvex();

  useEffect(() => {
    // Set auth
    convex.setAuth(async () => await getToken());

    // Clear auth
    // convex.clearAuth();
  }, [convex]);
}

Connection State

import { useConvexConnectionState } from 'convex/react';

function ConnectionIndicator() {
  const { isWebSocketConnected, hasInflightRequests } = useConvexConnectionState();

  return (
    <div>
      {!isWebSocketConnected && <div>Reconnecting...</div>}
      {hasInflightRequests && <div>Loading...</div>}
    </div>
  );
}

Non-React Browser Client

import { ConvexClient } from 'convex/browser';
import { api } from './convex/_generated/api';

const client = new ConvexClient('https://your-deployment.convex.cloud');

// One-time query
const messages = await client.query(api.messages.list);

// Reactive subscription
const unsubscribe = client.onUpdate(
  api.messages.list,
  {},
  (messages) => {
    console.log('Updated:', messages);
    updateUI(messages);
  }
);

// Later: clean up
unsubscribe();

// Execute mutation
const id = await client.mutation(api.messages.send, {
  author: 'Alice',
  body: 'Hello!',
});

// HTTP-only client (no WebSocket)
import { ConvexHttpClient } from 'convex/browser';

const httpClient = new ConvexHttpClient('https://your-deployment.convex.cloud');
const messages = await httpClient.query(api.messages.list, {});

Common Patterns

Load Multiple Queries

function Dashboard() {
  const messages = useQuery(api.messages.list);
  const users = useQuery(api.users.list);
  const stats = useQuery(api.stats.get);

  if (!messages || !users || !stats) return <div>Loading...</div>;

  return <div>{/* Use all data */}</div>;
}

// Alternative: useQueries
import { useQueries } from 'convex/react';

const { messages, users, stats } = useQueries({
  messages: { query: api.messages.list, args: {} },
  users: { query: api.users.list, args: {} },
  stats: { query: api.stats.get, args: {} },
});

Dependent Queries

function UserPosts({ userId }: { userId: string }) {
  const user = useQuery(api.users.get, { id: userId });
  const posts = useQuery(
    api.posts.getByUser,
    user ? { userId: user._id } : 'skip'
  );

  if (!user) return <div>Loading user...</div>;
  if (!posts) return <div>Loading posts...</div>;

  return <div>{/* Render */}</div>;
}

Mutation with Navigation

import { useMutation } from 'convex/react';
import { useNavigate } from 'react-router-dom';

function CreatePost() {
  const navigate = useNavigate();
  const create = useMutation(api.posts.create);

  const handleCreate = async (data: any) => {
    const postId = await create(data);
    navigate(`/posts/${postId}`);
  };

  return <button onClick={() => handleCreate({...})}>Create</button>;
}

Form Handling

function CommentForm({ postId }: { postId: string }) {
  const addComment = useMutation(api.comments.add);
  const [text, setText] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!text.trim()) return;

    await addComment({ postId, text });
    setText('');  // Clear form
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea value={text} onChange={e => setText(e.target.value)} />
      <button type="submit">Add Comment</button>
    </form>
  );
}