Recipes

With the right abstractions, you and your agents can make a lot of progress with a lot less code. Take a look at some of what's possible below.

P.S we made an Instant app just for you! Share this with your friends and you can play with every example together.

Loading...

Todos

84 lines
2 previews
Todos.tsx
1import { id, init } from '@instantdb/react';23const db = init({4  appId: "__YOUR_APP_ID__",5});67export default function InstantTodos() {8  const { data, isLoading, error } = db.useQuery({ todos: {} });910  if (error) return <p className="p-4 text-red-500">Oops, something broke</p>;1112  return (13    <div className="mx-auto flex h-full max-w-md flex-col gap-4 px-4 pt-8">14      <h1 className="text-xl font-semibold tracking-tight text-gray-800">15        instado16      </h1>17      <form18        className="flex gap-2"19        onSubmit={(e) => {20          e.preventDefault();21          const input = e.currentTarget.elements.namedItem(22            'todo',23          ) as HTMLInputElement;24          if (!input.value) return;25          db.transact([26            db.tx.todos[id()].update({ text: input.value, completed: false }),27          ]);28          e.currentTarget.reset();29        }}30      >31        <input32          name="todo"33          type="text"34          placeholder="What needs to be done?"35          className="flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-orange-400"36        />37        <button38          type="submit"39          className="rounded-lg bg-orange-600 px-4 py-2 text-sm font-medium text-white hover:bg-orange-700"40        >41          Add42        </button>43      </form>44      {data?.todos.length ? (45        <ul className="flex flex-col">46          {data.todos.map((todo) => (47            <li48              key={todo.id}49              className="group flex items-center gap-3 rounded-lg px-1 py-2 hover:bg-gray-50"50            >51              <input52                type="checkbox"53                className="accent-orange-600"54                checked={todo.completed}55                onChange={(e) =>56                  db.transact([57                    db.tx.todos[todo.id].update({58                      completed: e.currentTarget.checked,59                    }),60                  ])61                }62              />63              <span64                className={`flex-1 text-sm ${todo.completed ? 'text-gray-300 line-through' : 'text-gray-700'}`}65              >66                {todo.text}67              </span>68              <button69                onClick={() => db.transact([db.tx.todos[todo.id].delete()])}70                className="text-gray-300 opacity-0 transition-opacity group-hover:opacity-100 hover:text-gray-500"71              >72                ×73              </button>74            </li>75          ))}76        </ul>77      ) : isLoading ? null : (78        <p className="text-sm text-gray-400 italic">79          No todos just yet! Create your first one :)80        </p>81      )}82    </div>83  );84}
 
 

Cursors

38 lines
2 previews
Cursors.tsx
1import { Cursors, init } from '@instantdb/react';2import { useRef } from 'react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78export default function InstantCursors() {9  const room = db.room('cursors-example', '123');10  const color = useRef(randomDarkColor()).current;11  return (12    <Cursors room={room} userCursorColor={color} className={cursorsClassNames}>13      <span className="text-sm text-gray-400 italic">14        Move your cursor around!15      </span>16    </Cursors>17  );18}1920function randomDarkColor() {21  return (22    '#' +23    [0, 0, 0]24      .map(() =>25        Math.floor(Math.random() * 200)26          .toString(16)27          .padStart(2, '0'),28      )29      .join('')30  );31}3233const cursorsClassNames = [34  'flex h-full w-full items-center justify-center overflow-hidden touch-none',35  'bg-white',36  'bg-[radial-gradient(circle,#e0ddd5_1px,transparent_1px)]',37  'bg-[length:24px_24px]',38].join(' ');
 
 

Custom cursors

63 lines
2 previews
CustomCursors.tsx
1import { Cursors, id, init } from '@instantdb/react';2import { useRef } from 'react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78function CustomCursor({ name }: { name: string }) {9  return (10    <img11      src={`https://instantdb.com/api/avatar?name=${encodeURIComponent(name)}&size=40`}12      width={40}13      height={40}14      loading="eager"15      decoding="async"16      alt=""17      className="h-10 w-10"18    />19  );20}2122export default function InstantCursors() {23  const room = db.room('cursors-example', '124');24  const userId = useRef(id()).current;25  const color = useRef(randomDarkColor()).current;2627  db.rooms.useSyncPresence(room, {28    name: userId,29  });3031  return (32    <Cursors33      room={room}34      renderCursor={(props) => <CustomCursor name={props.presence.name} />}35      userCursorColor={color}36      className={cursorsClassNames}37    >38      <span className="text-sm text-gray-400 italic">39        You can customize your cursors too!40      </span>41    </Cursors>42  );43}4445function randomDarkColor() {46  return (47    '#' +48    [0, 0, 0]49      .map(() =>50        Math.floor(Math.random() * 200)51          .toString(16)52          .padStart(2, '0'),53      )54      .join('')55  );56}5758const cursorsClassNames = [59  'flex h-full w-full items-center justify-center overflow-hidden',60  'bg-white',61  'bg-[radial-gradient(circle,#e0ddd5_1px,transparent_1px)]',62  'bg-[length:24px_24px]',63].join(' ');
 
 

Reactions

135 lines
2 previews
Reactions.tsx
1import { RefObject, createRef, useRef } from 'react';2import { init } from '@instantdb/react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78export default function InstantTopics() {9  const room = db.room('topics-example', '123');10  const publishEmoji = db.rooms.usePublishTopic(room, 'emoji');1112  db.rooms.useTopicEffect(13    room,14    'emoji',15    ({ name, directionAngle, rotationAngle }) => {16      const emojiName = name as EmojiName;17      if (!emoji[emojiName]) return;1819      animateEmoji(20        { emoji: emoji[emojiName], directionAngle, rotationAngle },21        elRefsRef.current[name].current,22      );23    },24  );2526  const elRefsRef = useRef<{27    [k: string]: RefObject<HTMLDivElement>;28  }>(refsInit());2930  return (31    <div className={containerClassNames}>32      <div className="flex gap-4">33        {emojiNames.map((name) => (34          <div className="relative" key={name} ref={elRefsRef.current[name]}>35            <button36              className={emojiButtonClassNames}37              onClick={() => {38                const params = {39                  name,40                  rotationAngle: Math.random() * 360,41                  directionAngle: Math.random() * 360,42                };43                animateEmoji(44                  {45                    emoji: emoji[name],46                    rotationAngle: params.rotationAngle,47                    directionAngle: params.directionAngle,48                  },49                  elRefsRef.current[name].current,50                );5152                publishEmoji(params);53              }}54            >55              {emoji[name]}56            </button>57          </div>58        ))}59      </div>60    </div>61  );62}6364type EmojiName = keyof typeof emoji;6566const emoji = {67  fire: '🔥',68  wave: '👋',69  confetti: '🎉',70  heart: '❤️',71} as const;7273const emojiNames = Object.keys(emoji) as EmojiName[];7475function refsInit() {76  return Object.fromEntries(77    emojiNames.map((name) => [name, createRef<HTMLDivElement>()]),78  );79}8081const containerClassNames = [82  'flex h-full w-full items-center justify-center overflow-hidden select-none',83  'bg-[radial-gradient(circle,#e0ddd5_1px,transparent_1px)]',84  'bg-[length:24px_24px]',85].join(' ');8687const emojiButtonClassNames =88  'rounded-xl bg-white p-3 text-3xl shadow-md ring-1 ring-gray-100 transition duration-200 ease-in-out hover:-translate-y-1 hover:shadow-xl active:scale-95';8990function animateEmoji(91  config: { emoji: string; directionAngle: number; rotationAngle: number },92  target: HTMLDivElement | null,93) {94  if (!target) return;9596  const rootEl = document.createElement('div');97  const directionEl = document.createElement('div');98  const spinEl = document.createElement('div');99100  spinEl.innerText = config.emoji;101  directionEl.appendChild(spinEl);102  rootEl.appendChild(directionEl);103  target.appendChild(rootEl);104105  style(rootEl, {106    transform: `rotate(${config.directionAngle * 360}deg)`,107    position: 'absolute',108    top: '0',109    left: '0',110    right: '0',111    bottom: '0',112    margin: 'auto',113    zIndex: '9999',114    pointerEvents: 'none',115  });116117  style(spinEl, {118    transform: `rotateZ(${config.rotationAngle * 400}deg)`,119    fontSize: `40px`,120  });121122  setTimeout(() => {123    style(directionEl, {124      transform: `translateY(40vh) scale(2)`,125      transition: 'all 400ms',126      opacity: '0',127    });128  }, 20);129130  setTimeout(() => rootEl.remove(), 800);131}132133function style(el: HTMLElement, styles: Partial<CSSStyleDeclaration>) {134  Object.assign(el.style, styles);135}
 
 

Typing indicator

66 lines
2 previews
TypingIndicator.tsx
1import { id, init } from '@instantdb/react';2import { useRef } from 'react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78export default function InstantTypingIndicator() {9  const room = db.room('typing-indicator-example', '1234');10  const userId = useRef(id()).current;11  db.rooms.useSyncPresence(room, { id: userId });1213  const presence = db.rooms.usePresence(room);14  const { active, inputProps } = db.rooms.useTypingIndicator(15    room,16    'chat-input',17  );1819  const peers = Object.values(presence.peers);20  const activeSet = new Set(active.map((p) => p.id));2122  return (23    <div className="flex h-full">24      <div className="flex w-12 flex-col items-center gap-2 border-r border-gray-100 py-3">25        {peers.map((peer) => (26          <div key={peer.id} className="relative">27            <img28              src={`https://instantdb.com/api/avatar?name=${encodeURIComponent(peer.id || '')}&size=32`}29              alt=""30              className="h-8 w-8 rounded-full"31            />32            {activeSet.has(peer.id) ? (33              <div className="absolute -right-1 bottom-0 rounded-xs bg-black px-1 text-[10px] leading-3 text-white">3435              </div>36            ) : null}37          </div>38        ))}39      </div>40      <div className="flex flex-1 flex-col">41        <div className="flex flex-1 items-center justify-center">42          <p className="text-sm text-gray-400 italic">43            Start typing to see the indicator!44          </p>45        </div>46        <div className="px-3 pt-1 pb-3">47          <div className="truncate text-xs text-gray-500">48            {active.length ? typingInfo(active) : <>&nbsp;</>}49          </div>50          <textarea51            placeholder="Write a message..."52            className="w-full resize-none rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-orange-400"53            rows={1}54            onKeyDown={(e) => inputProps.onKeyDown(e)}55            onBlur={() => inputProps.onBlur()}56          />57        </div>58      </div>59    </div>60  );61}6263function typingInfo(active: unknown[]) {64  if (active.length === 1) return '1 person is typing...';65  return `${active.length} people are typing...`;66}
 
 

Avatar stack

49 lines
2 previews
AvatarStack.tsx
1import { id, init } from '@instantdb/react';2import { useRef } from 'react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78export default function InstantAvatarStack() {9  const room = db.room('avatars-example', 'avatars-example-1234');10  const userId = useRef(id()).current;1112  const presence = room.usePresence({ user: true });13  db.rooms.useSyncPresence(room, { name: userId.slice(0, 6) });1415  const peerCount = Object.keys(presence.peers).length;1617  return (18    <div className="flex h-full items-center justify-center px-4">19      <div className="flex flex-col gap-2">20        <div className="text-xs font-semibold tracking-wide text-gray-400 uppercase">21          Online — {peerCount + 1}22        </div>23        {presence.user ? <AvatarRow name={presence.user.name} /> : null}24        {Object.entries(presence.peers).map(([peerId, peer]) => (25          <AvatarRow key={peerId} name={peer.name} />26        ))}27        <p className="mt-1 text-xs text-gray-400 italic">28          Add more previews to see more avatars!29        </p>30      </div>31    </div>32  );33}3435function AvatarRow({ name }: { name: string }) {36  return (37    <div className="flex items-center gap-2.5">38      <div className="relative">39        <img40          src={`https://instantdb.com/api/avatar?name=${encodeURIComponent(name)}&size=32`}41          alt={name}42          className="h-8 w-8 rounded-full"43        />44        <div className="absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full border-2 border-white bg-green-500" />45      </div>46      <span className="text-sm font-medium text-gray-700">{name}</span>47    </div>48  );49}
 
 

Merge tile game

126 lines
2 previews
MergeTileGame.tsx
1/*2 * Tile Game!3 * This example is meant to mimic a simple collaborative game. We use a 4x4 grid4 * that users can color. We use `merge` to update a slice of data without5 * overwriting potential changes from other clients.6 * */78import { useEffect, useState } from 'react';9import { init } from '@instantdb/react';1011const db = init({12  appId: "__YOUR_APP_ID__",13});1415export default function App() {16  const room = db.room('tile-game-example');17  const [myColor, setMyColor] = useState<string | null>(null);18  const { isLoading, error, data } = db.useQuery({19    boards: { $: { where: { id: boardId } } },20  });21  const {22    peers,23    publishPresence,24    isLoading: isPresenceLoading,25  } = db.rooms.usePresence(room);2627  const boardState = data?.boards[0]?.state;2829  useEffect(() => {30    if (isLoading || isPresenceLoading || error) return;3132    // If the board doesn't exist, create it33    if (!boardState) {34      db.transact([db.tx.boards[boardId].update({ state: makeEmptyBoard() })]);35    }3637    // If I don't have a color, generate one and publish it38    // make sure to not choose a color that a peer has already chosen39    if (!myColor) {40      const takenColors = new Set(Object.values(peers).map((p) => p.color));41      const available = colors.filter((c) => !takenColors.has(c));42      const color =43        available[Math.floor(Math.random() * available.length)] || colors[0];44      setMyColor(color);45      publishPresence({ color });46    }47  }, [isLoading, isPresenceLoading, error, boardState]);4849  if (error)50    return <div className="p-4 text-sm text-red-500">{error.message}</div>;51  if (!boardState || isLoading || isPresenceLoading) return null;5253  return (54    <div className="flex h-full flex-col items-center justify-center gap-4 bg-[radial-gradient(circle,#e0ddd5_1px,transparent_1px)] bg-[length:24px_24px] p-4">55      {/* Header */}56      <div className="flex w-full max-w-[200px] items-center justify-between">57        <div className="flex items-center gap-2">58          <div59            className="h-3 w-3 rounded-full"60            style={{ backgroundColor: myColor ?? '#ddd' }}61          />62          <span className="text-xs text-gray-500">Your color</span>63        </div>64        <button65          className="text-xs text-gray-400 hover:text-gray-600"66          onClick={() => {67            db.transact([68              db.tx.boards[boardId].update({ state: makeEmptyBoard() }),69            ]);70          }}71        >72          Reset73        </button>74      </div>7576      {/* Board */}77      <div className="grid grid-cols-4 gap-1 rounded-xl border border-gray-200 bg-white p-2 shadow-sm">78        {Array.from({ length: boardSize }).map((_, r) =>79          Array.from({ length: boardSize }).map((_, c) => {80            const key = `${r}-${c}`;81            return (82              <div83                key={key}84                className="h-11 w-11 cursor-pointer rounded-lg transition-colors hover:brightness-95"85                style={{86                  backgroundColor: boardState[key] || emptyColor,87                }}88                onClick={() => {89                  db.transact([90                    db.tx.boards[boardId].merge({91                      state: { [key]: myColor },92                    }),93                  ]);94                }}95              />96            );97          }),98        )}99      </div>100    </div>101  );102}103104const boardSize = 4;105const emptyColor = '#f5f3f0';106107const colors = [108  '#e76f51', // warm red109  '#2a9d8f', // teal110  '#e9c46a', // amber111  '#264653', // dark teal112  '#f4a261', // orange113  '#d4a0d0', // lavender114];115116const boardId = '83c059e2-ed47-42e5-bdd9-6de88d26c521';117118function makeEmptyBoard() {119  const board: Record<string, string> = {};120  for (let r = 0; r < boardSize; r++) {121    for (let c = 0; c < boardSize; c++) {122      board[`${r}-${c}`] = emptyColor;123    }124  }125  return board;126}
 
 

Auth

141 lines
2 previews
Auth.tsx
1import { useState } from 'react';2import { init } from '@instantdb/react';34const db = init({5  appId: "__YOUR_APP_ID__",6});78export default function InstantAuth() {910  return (11    <div className={cls.root}>12      <db.SignedIn>13        <Dashboard />14      </db.SignedIn>15      <db.SignedOut>16        <Login />17      </db.SignedOut>18    </div>19  );20}2122function Dashboard() {23  const user = db.useUser();2425  return (26    <div className={cls.card}>27      <h2 className={cls.heading}>Welcome!</h2>28      <p className={cls.description}>29        You are signed in as <strong>{user.email}</strong>30      </p>31      <button className={cls.secondaryButton} onClick={() => db.auth.signOut()}>32        Sign Out33      </button>34    </div>35  );36}3738function Login() {39  const [state, setState] = useState({40    sentEmail: '',41    email: '',42    code: '',43    error: '',44  });4546  const { sentEmail, email, code, error } = state;4748  if (!sentEmail) {49    return (50      <form51        className={cls.card}52        onSubmit={async (e) => {53          e.preventDefault();54          if (!email) return;55          setState({ ...state, sentEmail: email, error: '' });56          try {57            await db.auth.sendMagicCode({ email });58          } catch (err: any) {59            setState({ ...state, sentEmail: '', error: err.body?.message });60          }61        }}62      >63        <h2 className={cls.heading}>Let's log you in</h2>64        <p className={cls.description}>65          Enter your email and we'll send you a verification code.66        </p>67        <input68          className={cls.input}69          placeholder="Enter your email"70          type="email"71          value={email}72          onChange={(e) =>73            setState({ ...state, email: e.target.value, error: '' })74          }75        />76        <button type="submit" className={cls.button} disabled={!email.trim()}>77          Send Code78        </button>79        {error ? <p className={cls.error}>{error}</p> : null}80      </form>81    );82  }8384  return (85    <form86      className={cls.card}87      onSubmit={async (e) => {88        e.preventDefault();89        if (!code) return;90        try {91          await db.auth.signInWithMagicCode({ email: sentEmail, code });92        } catch (err: any) {93          setState({ ...state, error: err.body?.message });94        }95      }}96    >97      <h2 className={cls.heading}>Enter your code</h2>98      <p className={cls.description}>99        We sent a code to <strong>{sentEmail}</strong>100      </p>101      <input102        autoFocus103        className={cls.input}104        type="text"105        inputMode="numeric"106        placeholder="Verification code"107        value={code}108        onChange={(e) =>109          setState({ ...state, code: e.target.value, error: '' })110        }111      />112      <button type="submit" className={cls.button} disabled={!code.trim()}>113        Verify Code114      </button>115      <button116        type="button"117        className={cls.secondaryButton}118        onClick={() =>119          setState({ sentEmail: '', email: '', code: '', error: '' })120        }121      >122        Back123      </button>124      {error ? <p className={cls.error}>{error}</p> : null}125    </form>126  );127}128129const cls = {130  root: 'flex h-full items-center justify-center px-2',131  card: 'flex w-full max-w-xs flex-col gap-3',132  heading: 'text-lg font-bold',133  description: 'text-sm text-gray-600',134  input:135    'rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-orange-400 w-full',136  button:137    'rounded-lg bg-orange-600 px-4 py-2 text-sm font-medium text-white hover:bg-orange-700 w-full disabled:opacity-50',138  secondaryButton:139    'rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 w-full',140  error: 'text-red-700 text-sm bg-red-50 border border-red-500 rounded-sm p-2',141};