mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
docs(react): add documentation and tests for initialValues and newThreadId options
- Document initialValues for cached thread display - Document newThreadId for optimistic thread creation - Add comprehensive test coverage for both features
This commit is contained in:
@@ -503,6 +503,82 @@ const handleSubmit = (text: string) => {
|
||||
};
|
||||
```
|
||||
|
||||
### Cached Thread Display
|
||||
|
||||
Use the `initialValues` option to display cached thread data immediately while the official history is being loaded from the server. This improves user experience by showing cached data instantly when navigating to existing threads.
|
||||
|
||||
```tsx
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
const CachedThreadExample = ({ threadId, cachedThreadData }) => {
|
||||
const stream = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
threadId,
|
||||
// Show cached data immediately while history loads
|
||||
initialValues: cachedThreadData?.values,
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{stream.messages.map((message) => (
|
||||
<div key={message.id}>{message.content as string}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
The values flow follows this priority:
|
||||
1. **Initial load**: Shows `initialValues` while history loads
|
||||
2. **During submit**: `optimisticValues` take precedence
|
||||
3. **After server response**: Official history replaces all
|
||||
|
||||
### Optimistic Thread Creation
|
||||
|
||||
Use the `newThreadId` option to enable optimistic UI patterns where you need to know the thread ID before the thread is actually created. Namely, when the `threadId` is left `null`, `useStream` will under the hood create a thread using the `newThreadId`.
|
||||
|
||||
```tsx
|
||||
import { useState } from "react";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
|
||||
const OptimisticThreadExample = () => {
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
const [optimisticThreadId] = useState(() => crypto.randomUUID());
|
||||
|
||||
const stream = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
threadId, // null initially
|
||||
newThreadId: optimisticThreadId, // predetermined ID for new thread
|
||||
onThreadId: setThreadId, // update threadId after creation
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
const handleSubmit = (text: string) => {
|
||||
// Can immediately navigate to /threads/optimisticThreadId
|
||||
// without waiting for thread creation
|
||||
window.history.pushState({}, "", `/threads/${optimisticThreadId}`);
|
||||
|
||||
stream.submit({ messages: [{ type: "human", content: text }] });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Thread ID: {threadId || optimisticThreadId} (optimistic)</p>
|
||||
{/* Rest of component */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Usage pattern:**
|
||||
- Set `threadId: null` and `newThreadId: "predetermined-id"`
|
||||
- Submit message to create thread with the specified ID
|
||||
- Use `onThreadId` callback to update `threadId` after creation
|
||||
- Navigate optimistically to routes using the predetermined ID
|
||||
|
||||
### TypeScript
|
||||
|
||||
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
|
||||
|
||||
@@ -138,4 +138,132 @@ describe("useStream", () => {
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays initial values immediately", () => {
|
||||
const initialValues = {
|
||||
messages: [
|
||||
{ id: "cached-1", type: "human", content: "Cached user message" },
|
||||
{ id: "cached-2", type: "ai", content: "Cached AI response" },
|
||||
],
|
||||
};
|
||||
|
||||
function TestCachedComponent() {
|
||||
const { messages, values } = useStream({
|
||||
assistantId: "test-assistant",
|
||||
apiKey: "test-api-key",
|
||||
initialValues,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.id ?? i} data-testid={`message-${i}`}>
|
||||
{typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div data-testid="values">{JSON.stringify(values)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestCachedComponent />);
|
||||
|
||||
// Should immediately show cached messages
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Cached user message");
|
||||
expect(screen.getByTestId("message-1")).toHaveTextContent("Cached AI response");
|
||||
|
||||
// Values should include initial values
|
||||
expect(screen.getByTestId("values")).toHaveTextContent("Cached user message");
|
||||
});
|
||||
|
||||
it("handles null initial values", () => {
|
||||
function TestNullInitialComponent() {
|
||||
const { messages, values } = useStream({
|
||||
assistantId: "test-assistant",
|
||||
apiKey: "test-api-key",
|
||||
initialValues: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="message-count">{messages.length}</div>
|
||||
<div data-testid="values">{JSON.stringify(values)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestNullInitialComponent />);
|
||||
|
||||
// Should handle null initialValues gracefully
|
||||
expect(screen.getByTestId("message-count")).toHaveTextContent("0");
|
||||
expect(screen.getByTestId("values")).toHaveTextContent("{}");
|
||||
});
|
||||
|
||||
it("accepts newThreadId option without errors", () => {
|
||||
// Test that newThreadId option can be passed without causing errors
|
||||
function TestNewThreadComponent() {
|
||||
const stream = useStream({
|
||||
assistantId: "test-assistant",
|
||||
apiKey: "test-api-key",
|
||||
threadId: null, // Start with no thread
|
||||
newThreadId: "predetermined-thread-id",
|
||||
onThreadId: () => {}, // Mock callback
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">
|
||||
{stream.isLoading ? "Loading..." : "Not loading"}
|
||||
</div>
|
||||
<div data-testid="thread-id">{stream.client ? "Client ready" : "No client"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestNewThreadComponent />);
|
||||
|
||||
// Should render without errors
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
expect(screen.getByTestId("thread-id")).toHaveTextContent("Client ready");
|
||||
});
|
||||
|
||||
it("shows initial values before any streaming", () => {
|
||||
const initialValues = {
|
||||
messages: [
|
||||
{ id: "initial-1", type: "human", content: "Initial message" },
|
||||
],
|
||||
customField: "initial-value"
|
||||
};
|
||||
|
||||
function TestInitialValuesComponent() {
|
||||
const stream = useStream({
|
||||
assistantId: "test-assistant",
|
||||
apiKey: "test-api-key",
|
||||
initialValues,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="message-0">
|
||||
{typeof stream.messages[0]?.content === "string"
|
||||
? stream.messages[0]?.content
|
||||
: JSON.stringify(stream.messages[0]?.content)}
|
||||
</div>
|
||||
<div data-testid="custom-field">{stream.values.customField}</div>
|
||||
<div data-testid="loading">{stream.isLoading ? "Loading" : "Not loading"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestInitialValuesComponent />);
|
||||
|
||||
// Should immediately show initial values without any loading
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Initial message");
|
||||
expect(screen.getByTestId("custom-field")).toHaveTextContent("initial-value");
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user