---
title: Integration Recipes
description: Copy‑paste patterns for the screens you're actually going to build.
---

A handful of common screens, with the code and a sentence of why you'd use each. Take them, paste them, restyle them.

## Sign‑in gate

**When**: on app load, you want to show a sign‑in screen until the user has authenticated.

```tsx
import { useThinkworkAuth } from "@thinkwork/react-native-sdk";

function App() {
  const { status } = useThinkworkAuth();
  if (status === "unknown") return <Splash />;
  if (status !== "signed-in") return <SignInScreen />;
  return <AppShell />;
}

function SignInScreen() {
  const { signInWithGoogle } = useThinkworkAuth();
  return <Button onPress={signInWithGoogle} title="Continue with Google" />;
}
```

## Agent picker with persistent selection

**When**: the user has several agents and you want their last choice to stick across app launches.

The SDK gives you the agent list but intentionally stays out of persistence — different apps want different storage (AsyncStorage, MMKV, whatever). This is the typical shape:

```tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { Agent } from "@thinkwork/react-native-sdk";

const KEY = "myapp.selectedAgentId";

export function useSelectedAgent(agents: Agent[]) {
  const [selectedId, setSelectedId] = React.useState<string | null>(null);

  React.useEffect(() => {
    AsyncStorage.getItem(KEY).then(setSelectedId);
  }, []);

  const selected = agents.find(a => a.id === selectedId) ?? agents[0] ?? null;

  const setId = (id: string | null) => {
    setSelectedId(id);
    id ? AsyncStorage.setItem(KEY, id) : AsyncStorage.removeItem(KEY);
  };

  return { selectedAgent: selected, setSelectedAgentId: setId };
}
```

Use it:

```tsx
const { user } = useThinkworkAuth();
const { agents } = useAgents({ tenantId: user?.tenantId });
const { selectedAgent, setSelectedAgentId } = useSelectedAgent(agents);
```

## Thread list screen

**When**: you're showing the user their threads.

```tsx
const { threads, loading, refetch } = useThreads({ tenantId, agentId: selectedAgent?.id });

return (
  <FlatList
    data={threads}
    keyExtractor={(t) => t.id}
    renderItem={({ item }) => <ThreadRow thread={item} />}
    refreshing={loading}
    onRefresh={refetch}
  />
);
```

## Chat screen

**When**: the user has tapped into a thread and you're showing messages and a composer.

A minimal version:

```tsx
function ChatScreen({ threadId }: { threadId: string }) {
  const { thread } = useThread(threadId);
  const { messages } = useMessages(threadId);
  const sendMessage = useSendMessage();
  const [draft, setDraft] = React.useState("");

  const submit = async () => {
    const text = draft.trim();
    if (!text) return;
    setDraft("");
    await sendMessage(threadId, text);
  };

  return (
    <>
      <Header title={thread?.title ?? "Chat"} />
      <FlatList
        data={[...messages].reverse()}
        keyExtractor={(m) => m.id}
        renderItem={({ item }) => <Bubble role={item.role} text={item.content} />}
      />
      <Composer value={draft} onChange={setDraft} onSend={submit} />
    </>
  );
}
```

A full‑featured version with markdown rendering and auto‑scroll lives in [`examples/chat-ui/ThinkworkChat.tsx`](https://github.com/thinkwork-ai/thinkwork/blob/main/packages/react-native-sdk/examples/chat-ui/ThinkworkChat.tsx) — copy it, restyle it.

## Start a new thread (atomic)

**When**: your user types a message in a "compose" box and you want both the thread and the first message created in one call.

```tsx
const createThread = useCreateThread();

async function start(text: string) {
  const thread = await createThread({
    tenantId,
    agentId: selectedAgent.id,
    title: text.slice(0, 60),
    channel: "CHAT",
    firstMessage: text,
  });
  router.push(`/thread/${thread.id}`);
}
```

One round trip. The agent starts responding as soon as the thread is created.

## Tab‑bar badge

**When**: you have a "Chat" or "Brain" tab and want the standard red unread count dot.

```tsx
function ChatTabBadge() {
  const { user } = useThinkworkAuth();
  const { agents } = useAgents({ tenantId: user?.tenantId });
  const { selectedAgent } = useSelectedAgent(agents);
  const { count } = useUnreadThreadCount({
    tenantId: user?.tenantId,
    agentId: selectedAgent?.id,
  });
  return count > 0 ? <Badge>{count}</Badge> : null;
}
```

When the user reads a thread, the count goes down automatically — no manual plumbing needed.

## Optimistic "just read" (optional polish)

**When**: you want the unread dot to disappear the instant a user taps a thread, before the server round trip returns.

Keep a local set alongside the server state:

```tsx
const locallyRead = useRef(new Set<string>());

function openThread(t: Thread) {
  locallyRead.current.add(t.id);
  updateThread(t.id, { lastReadAt: new Date().toISOString() });
  router.push(`/thread/${t.id}`);
}

function isUnread(t: Thread) {
  if (locallyRead.current.has(t.id)) return false;
  return /* normal unread logic */;
}
```

The server truth still catches up; the local set just shortcuts the UX.

## What to do when ThinkWork isn't configured

**When**: your app ships to environments where ThinkWork isn't set up (dev builds, pre‑activation, etc.) and you want to hide the features gracefully.

```tsx
const isThinkworkConfigured = Boolean(
  process.env.EXPO_PUBLIC_THINKWORK_GRAPHQL_URL &&
  process.env.EXPO_PUBLIC_COGNITO_USER_POOL_ID,
);

return isThinkworkConfigured ? (
  <ThinkworkProvider config={config}><App /></ThinkworkProvider>
) : <App />;
```

Hide tab‑bar entries that depend on ThinkWork behind the same check so users never see a broken tab.
