Skip to content

bug: infinite re-render loop because missing useCallback in useChat => handleInputChange #6266

Closed
tresorama/ai
#1
@tresorama

Description

@tresorama

Description

Was investigation why my chat re-render so much, and found that inside useChat, handleInputChange is created without useCallback, resulting on different function every time the hook is called.

const handleInputChange = (e: any) => {
setInput(e.target.value);
};

Exploration

export const Chat = ({
  initialMessages,
  onStreamingComplete,
}: {
  initialMessages?: UIMessage[],
  onStreamingComplete: (args: { chat: ReturnType<typeof useChat>; }) => void,
}) => {

  // server state
  const useChatOptions = useMemo<UseChatOptions>(
    () => {
      return {
        id: 'FIXED',
        api: 'https://...',
        initialMessages: initialMessages ? initialMessages : undefined,
        onError: error => {
          console.error('An error occurred:', error);
          toast.error('An error occurred. We are investingating! Try different model. \nCODE ' + error.message);
        },
      };
    },
    [
      initialMessages,
    ]
  );
  const chat = useChat(useChatOptions);
  useLogDifferencesBetweenPrevAndCurrentValue(chat); // Utility hook 

  return <ChatUI chat={chat} />;

};
Image

Utility hook used for debug

I created this utility debug hook to check what changes between re-render

import { useEffect, useRef } from "react";

type AnyObject = { [k: string]: unknown; };


const getDifferences = (
  prev: AnyObject,
  current: AnyObject
) => {

  type Difference = {
    key: string,
    type: 'added' | 'removed' | 'changed',
    v: {
      prev: unknown,
      current: unknown,
    },
    isEqual_withObjectIs?: boolean,
    isEqual_withJsonStringify?: boolean,
  };
  const differences: Difference[] = [];

  // get all keys
  const allKeys = Array.from(
    new Set([...Object.keys(prev), ...Object.keys(current)])
  );

  // create diff
  allKeys.forEach(key => {
    const prevV = prev[key];
    const currentV = current[key];

    // if new prop
    if (!prevV && currentV) {
      differences.push({
        key,
        type: 'added',
        v: {
          prev: prevV,
          current: currentV,
        }
      });
      return;
    }

    // if removed prop
    if (prevV && !currentV) {
      differences.push({
        key,
        type: 'removed',
        v: {
          prev: prevV,
          current: currentV,
        }
      });
      return;
    }

    // if not changed
    const isEqual_withObjectIs = Object.is(prevV, currentV);
    const isEqual_withJsonStringify = JSON.stringify(prevV) === JSON.stringify(currentV);
    if (isEqual_withObjectIs && isEqual_withJsonStringify) return;

    // if changed
    differences.push({
      key,
      type: 'changed',
      v: {
        prev: prevV,
        current: currentV,
      },
      isEqual_withObjectIs,
      isEqual_withJsonStringify
    });


  });

  return differences;

};


export const useLogDifferencesBetweenPrevAndCurrentValue = (currentValue: AnyObject) => {
  const prevValue = useRef(currentValue);

  // on every render => log differences
  useEffect(() => {
    const differences = getDifferences(prevValue.current, currentValue);
    prevValue.current = currentValue;
    console.log({ differences });
  });

  return null;
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions