What is wrong with Langchain frontend sdk

I have a backend with custome endpoint that returns the correct sse format for langchain agent

And when i use this endpoint with langchain frontend sdk with react or next js

I noticed that it loses all the previous content or state the moment you send anything

I thought this was maybe my fault whether on the backend or the frontend

But when i actually tested an example code that was on their channel that uses this approach it also has this problem

The only time that it was working correctly is when you provide it with a running langchain server url

I don’t know if this is a bug or what but it’s very frustrating because it broke the ui

Please help

hi @AmgadEbaid

what langchain package version are you using? Python or JavaScript?

python -m langchain_core.sys_info
pip freeze | grep -E 'langchain|langgraph'

npm ls langchain @langchain/core @langchain/langgraph @langchain/langgraph-sdk @langchain/react

amgad@ c:\users\amgad ±- @langchain/core@1.1.8 ±- @langchain/google-genai@2.1.3 | `– @langchain/core@1.1.8 deduped ±- @langchain/langgraph-checkpoint-postgres@1.0.0 | ±- @langchain/core@1.1.8 deduped | `– @langchain/langgraph-checkpoint@1.0.0 | `– @langchain/core@1.1.8 deduped `– langchain@1.2.3 ±- @langchain/core@1.1.8 deduped `– @langchain/langgraph@1.0.7 ±- @langchain/core@1.1.8 deduped `– @langchain/langgraph-sdk@1.3.1 `– @langchain/core@1.1.8 deduped

run it in your project folder, not c:\users\amgad.
Run all three commands.

My code is old so i am not using react sdk

I have just taken a look on the docs for react sdk and i think i need to do some refactoring

But will it fix this behavior?!

The old frontend sdk name was something like langchain/sdk

±- @langchain/core@1.1.49 ±- @langchain/langgraph-sdk@1.5.5 | `– @langchain/core@1.1.49 deduped `– @langchain/react@1.0.23 ±- @langchain/core@1.1.49 deduped `– @langchain/langgraph-sdk@1.9.22 `– @langchain/core@1.1.49 deduped

langchain==1.2.7 langchain-cerebras==0.6.0 langchain-core==1.2.7 langchain-openai==0.3.35 langgraph==1.0.7 langgraph-api==0.7.13 langgraph-checkpoint==3.0.1 langgraph-cli==0.4.12 langgraph-prebuilt==1.0.7 langgraph-runtime-inmem==0.23.1 langgraph-sdk==0.3.1

Two findings from your dep dump:

1. You have two copies of @langchain/langgraph-sdk (1.5.5 + 1.9.22). Fix this first. @langchain/react@1.0.23 depends on the SDK, and the useStream hook, the transport (HttpAgentServerAdapter), and message reconciliation all live inside that package. Two copies = two module instances; a transport/client built from one version handed to a hook bound to the other → identity + values.messages reconciliation break, which looks exactly like “state wiped on submit.” Force one version:

"overrides": { "@langchain/langgraph-sdk": "1.9.22" }   // pnpm: "resolutions" for yarn
rm -rf node_modules package-lock.json && npm install
npm ls @langchain/langgraph-sdk      # must show ONE version, all deduped

2. “Will migrating to the React SDK fix it?” - no, not by itself. Old langchain/sdk and new @langchain/react useStream consume the same server contract. Durability comes from the backend (checkpointed thread state replayed in full each turn), not from the frontend package. Do migrate - but pair it with a real backend.

You already have langgraph-cli + langgraph-api, so just run the server:

langgraph dev    # → http://localhost:2024
useStream({ apiUrl: "http://localhost:2024", assistantId: "agent" })

That’s why the official server URL works - it persists thread state and replays the full message list.

Caveat: langgraph-runtime-inmem keeps state in process memory - fine for dev, but lost on restart and across workers/serverless. For production back it with Postgres (@langchain/langgraph-checkpoint-postgres, which you already had in your other project).

If you insist on your own custom HTTP endpoint: use useStream({ transport: { stream } }) + toLangGraphEventStreamResponse(...), and emit the full values.messages snapshot every turn with stable IDs - a partial snapshot is treated as an explicit removal.

But i already used to emit the full values.messages

From the backend but on the frontend the time between sending a new request and receiving the response there is no state on the front end

Why is that, can’t they just keep the old state a little longer

Sorry if am not getting it and thank you very much for your help

update your packages and migrate to @langchain/react - RECOMMENDED. Then let’s follow up.

Do you use checkpointer for your backend agent with a dedicated thread_id per thread?

Or try this:

keep the reseed base in sync with the latest full state:

const [seed, setSeed] = useState({ messages: [] });
const stream = useStream({
  transport: myTransport,
  initialValues: seed,                 // base for the NEXT submit
  onFinish: (state) => setSeed(state.values),  // full final thread - no blank next time
});

This call back does not do anything, i believe it’s because i am using transport at least this is what chatgpt have told me

I have tried to log anything from it

And i have tried also to set some fake messages as initial values to see if they will appear during loading state but still even that doesn’t work

Can you share your code so I can reproduce what is happening there?

This exactly the code that i am using

To see the effect throttle the network to 3g

Also note onFinish wasn’t working because the package was old after i updated the package it worked and i have tried your method but it still doesn’t work

ok, let me check that example

@AmgadEbaid make ChatInterface like this:

"use client";

import { useCallback, useMemo } from "react";
import { type Message } from "@langchain/langgraph-sdk";
import { useStream, FetchStreamTransport } from "@langchain/langgraph-sdk/react";
import { AIMessage, ToolCall, ToolMessage } from "@langchain/core/messages";

import { WelcomeScreen } from "./Welcome";
import { ToolCallBubble, type ToolCallState } from "./ToolCall";
import { ErrorBubble } from "./ErrorBubble";
import { ChatInput } from "./ChatInput";
import { isAIMessage, isToolMessage, isHumanMessage, extractTextContent } from "../utils";

interface ChatInterfaceProps {
  apiKey: string;
}

export default function ChatInterface({ apiKey }: ChatInterfaceProps) {
  // Create transport with API key - using closure to capture ref
  // Refs are only accessed in async callbacks (onRequest), not during render
  const transport = useMemo(() => {
    const apiKeyValue = apiKey;
    return new FetchStreamTransport({
      apiUrl: "/api/basic",
      onRequest: async (url: string, init: RequestInit) => {
        const customBody = JSON.stringify({
          ...(JSON.parse(init.body as string) || {}),
          apiKey: apiKeyValue,
        });

        return {
          ...init,
          body: customBody,
        };
      },
    });
  }, [apiKey]);

  const stream = useStream({
    transport,
  });

  // Extract tool calls from messages
  // Use a Map keyed by message reference to avoid ID mismatches
  const toolCallsByMessage = useMemo(() => {
    const map = new Map<Message, ToolCallState[]>();

    stream.messages.forEach((message) => {
      // Only process AI messages (check both SDK format and LangChain Core format)
      if (!isAIMessage(message)) {
        return
      };

      const aiMessage = message as AIMessage;

      // Extract tool calls from AIMessage - check both direct property and kwargs
      let toolCalls: ToolCall[] = [];

      // Check for tool_calls directly on message (SDK format)
      if (aiMessage.tool_calls && Array.isArray(aiMessage.tool_calls)) {
        toolCalls = aiMessage.tool_calls as ToolCall[];
      }

      // Extract tool messages (responses) - find ToolMessage type messages
      const toolMessages: ToolMessage[] = [];
      for (const msg of stream.messages) {
        if (isToolMessage(msg)) {
          const toolMessage = msg as ToolMessage;
          const toolCallId = toolMessage.tool_call_id;

          if (toolCallId && toolCalls.some((tc) => tc.id === toolCallId)) {
            toolMessages.push(msg as ToolMessage);
          }
        }
      }

      // Build tool call states
      if (toolCalls.length > 0) {
        const toolCallStates: ToolCallState[] = [];
        for (const toolCall of toolCalls) {
          const toolMessage = toolMessages.find((tm) => {
            return tm.tool_call_id === toolCall.id;
          });

          toolCallStates.push({
            toolCall,
            toolMessage,
          });
        }
        map.set(message, toolCallStates);
      }
    });

    return map;
  }, [stream.messages]);

  const handleSend = useCallback(
    (messageOverride?: string) => {
      const messageToSend = messageOverride || "";

      if (!messageToSend.trim() || stream.isLoading) {
        return;
      }

      if (!apiKey.trim()) {
        // Add error message to stream
        return;
      }

      // Capture the messages already on screen before submitting. The custom
      // transport resets stream values to `initialValues` (`{}`) on submit, so
      // without an optimistic value the chat blanks out until the first server
      // `values` event arrives (i.e. after the first model round-trip).
      const previousMessages = stream.messages;
      const optimisticHuman = {
        type: "human" as const,
        content: messageToSend,
        id: crypto.randomUUID(),
      };

      // Submit message using stream API
      stream.submit(
        {
          messages: [{ content: messageToSend, type: "human" }],
        },
        {
          // Keep prior messages + the new one visible immediately. The server
          // `values` event later replaces this with the authoritative history.
          optimisticValues: () => ({
            messages: [...previousMessages, optimisticHuman],
          }),
        }
      );
    },
    [apiKey, stream]
  );

  const handleInputSubmit = useCallback(
    (message: string) => {
      handleSend(message);
    },
    [handleSend]
  );

  const isLoading = stream.isLoading;
  const errorMessage = stream.error instanceof Error ? stream.error.message : typeof stream.error === "string" ? stream.error : undefined;

  return (
    <div className="flex flex-col h-full">
      {/* Messages Area */}
      <div className="flex-1 overflow-y-auto px-6 py-8">
        {stream.messages.length === 0 ? (
          <div className="flex flex-col items-center justify-center h-full">
            <WelcomeScreen apiKey={apiKey} handleSend={handleSend} />
          </div>
        ) : (
          <div className="max-w-3xl mx-auto space-y-6">
            {/* Render messages - filter out tool messages as they're displayed separately */}
            {stream.messages
              .filter((message) => !isToolMessage(message))
              .map((message, messageIndex) => {
                // Get tool calls associated with this AI message
                const associatedToolCalls =
                  isAIMessage(message)
                    ? toolCallsByMessage.get(message) || []
                    : [];

                return (
                  <div key={message.id || messageIndex}>
                    {/* Message */}
                    {extractTextContent(message.content) !== "" && (
                      <div
                        className={`flex ${
                          isHumanMessage(message)
                            ? "justify-end"
                            : "justify-start"
                        }`}
                      >
                        <div
                          className={`max-w-[80%] rounded-lg px-4 py-3 ${
                            isHumanMessage(message)
                              ? "bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900"
                              : "bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100"
                          }`}
                        >
                          <p className="whitespace-pre-wrap">
                            {extractTextContent(message.content)}
                            {messageIndex ===
                              stream.messages.filter((m) => !isToolMessage(m)).length - 1 &&
                              isLoading && (
                                <span className="inline-block w-2 h-4 bg-gray-400 dark:bg-gray-600 ml-1 animate-pulse" />
                              )}
                          </p>
                        </div>
                      </div>
                    )}
                    {/* Tool calls associated with this message */}
                    {associatedToolCalls.map((toolCallState) => (
                      <ToolCallBubble
                        key={toolCallState.toolCall.id}
                        toolCallState={toolCallState}
                      />
                    ))}
                    {/* Error bubble associated with this message */}
                    {errorMessage &&
                      isAIMessage(message) &&
                      messageIndex ===
                        stream.messages.filter((m) => !isToolMessage(m)).length - 1 && (
                        <ErrorBubble error={errorMessage} />
                      )}
                  </div>
                );
              })}
            {isLoading && (
              <div className="flex justify-center items-center gap-1.5 py-2">
                <span
                  className="inline-block w-2 h-2 bg-gray-400 dark:bg-gray-600 rounded-full animate-dot-wave"
                  style={{ animationDelay: "0ms" }}
                />
                <span
                  className="inline-block w-2 h-2 bg-gray-400 dark:bg-gray-600 rounded-full animate-dot-wave"
                  style={{ animationDelay: "200ms" }}
                />
                <span
                  className="inline-block w-2 h-2 bg-gray-400 dark:bg-gray-600 rounded-full animate-dot-wave"
                  style={{ animationDelay: "400ms" }}
                />
              </div>
            )}
          </div>
        )}
      </div>

      {/* Input Area */}
      <ChatInput onSubmit={handleInputSubmit} isLoading={isLoading} />
    </div>
  );
}

You are the goat !

Thank you very much

update your packages and migrate to @langchain/react - RECOMMENDED. Then let’s follow up.

I am happy to support in this effort. We have an example for this. Let me know if you come across any issues.