skip to Main Content

I am trying to learn React and build a simple, but specialized, chatbot using OpenAI API along the way. I am currently able to send and receive messages, but I encounter an issue when displaying the messages. When I send a user message, the user message will display on the page, but only until the response arrives, at which the user message disappears.

My Chat component is shown below. My guess was that setMessages([...messages, botMessage]); simply appended a message to the state of the current list of messages, though maybe I have misunderstood something there.

import React, { useState } from "react";
import { PaperAirplaneIcon } from "@heroicons/react/20/solid";
import axios from "axios";
import UserMessage from "./UserMessage";
import BotMessage from "./BotMessage";

const Chat = () => {
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState("");
  const [loading, setLoading] = useState(false);
  const [rules, setRules] = useState([]);

  const handleInputSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    const userMessage = { type: "user", text: inputValue };
    setMessages([...messages, userMessage]);
    setInputValue("");
    const botMessage = await getBotMessage(inputValue);
    setMessages([...messages, botMessage]);
    setLoading(false);
  };

  const getBotMessage = async (userMessage) => {
    try {
      const response = await axios.post("http://<url>/api/chat", {
        message: userMessage,
      });
      console.log("response", response.data);
      const botMessage = { type: "bot", text: response.data.response };
      return botMessage;
    } catch (e) {
      console.log(e);
      const botMessage = {
        type: "bot",
        text: "Something went wrong. Try again.",
      };
      return botMessage;
    }
  };

  return (
    <div className="flex flex-col items-center w-full h-full justify-between">
      <h1 className="text-3xl font-bold my-5">Chatbot</h1>
      <div className="w-full">
        {messages.map((message, index) => {
          if (message.type === "user") {
            return <UserMessage key={index} text={message.text} />;
          }
          return <BotMessage key={index} text={message.text} />;
        })}
        <form
          onSubmit={handleInputSubmit}
          className="flex flex-row w-full mb-5 mt-2"
        >
          <input
            className="border-2 rounded-md p-2 w-full"
            type="text"
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
          />
          <button type="submit">
            <PaperAirplaneIcon className="w-6 h-6 text-green-500" />
          </button>
        </form>
      </div>
    </div>
  );
};

export default Chat;

2

Answers


  1. Chosen as BEST ANSWER

    EDIT: The solution below works, but is not the optimal solution. Check the other answer.

    As new to React, I don't understand why this is the way it is, but I found the answer.

    For some reason the setMessages([...messages, botMessage]); can't see the updated messages state array given by setMessages([...messages, userMessage]); three lines prior (when a user message is sent).

    The solution is to add both messages, when the bot message arrives like, so the handleInputSubmit becomes:

    const handleInputSubmit = async (e) => {
        e.preventDefault();
        setLoading(true);
        const userMessage = { type: "user", text: inputValue };
        setMessages([...messages, userMessage]);
        setInputValue("");
        const botMessage = await getBotMessage(inputValue);
        setMessages([...messages, userMessage, botMessage]); // <-- This fixes it.
        setLoading(false);
      };
    

    If anyone can explain or provide a link to documentation explaining this, I would very much appreciate it.


  2. You have the explanation in official documentation here:

    The set function only updates the state variable for the next render.
    If you read the state variable after calling the set function, you
    will still get the old value that was on the screen before your call.

    Your call to setMessages doesn’t result in your message variable being immediately updated. If you think about it, it makes total sense as messages is defined as a const, so it can never be reassigned! On the next re-render, you’ll have a fresh new messages with the updated value.

    So your second call to setMessages is adding botMessage to the original array, effectively replacing userMessage.

    Something you could try is using updater functions like:

    setMessages(prevState => [...prevState, userMessage]);
    ...
    setMessages(prevState => [...prevState, botMessage]);
    

    Updater functions are also executed on the next re-render, but they are applied in queue and always receive the most recent value of your state variable, so both elements will be properly added.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search