0

So I encountered a problem with Next.js / React and mutating an array that is saved in state. I can't really wrap my head around it and figure out why it happens and how to fix it.

I tried Google search and stackoverflow but to be honest this didn't help much and I am unsure what the exact problem is and what to do about it.

So here is what I was trying to achieve when the problem occurred.

I built a simple Chatbot / ChatGPT implementation with Next.js that allows me to input a question or request, and send it to my API / Backend which would then query ChatGPT itself and return the result.

For details of the implementation see the code below.

Here is a breakdown of how it should work:

  • The user enters the question into a simple HTML input
  • The user clicks the "Send" button
  • This triggers a method that first adds the input message to the array of all messages (input by user and output by ChatGPT), which should be displayed in a chronological order
  • Then the method does a fetch to the API / Backend which in turn calls the OpenAI API with the input
  • When there is a result (async) it gets added to the array of messages as well and should be displayed below the input message

What actually happens is that first I can see the input message, but after the fetch has completed it gets overwritten by the result message and I don't see both.

I am not completely sure why this happens. I know that React doesn't change the state immediately, but it happens asnyc. This could be one reason.

But I suspect the main reason is the closure that is formed to update the state. It uses the spread syntax to add the new message to the already existing array of messages. And in both cases when I update the array with the input and the result message, the closure does not have the updated state (array of messages), but it referes to an empty array. This is probably why the mutation of the array twice is not working and are overwriting each other, correct?

So is there an actual way to fix this?

Chatbot.tsx:

import {useState} from "react";

export default function Chatbot() {
    const [inputMessage, setInputMessage] = useState('');
    const [messages, setMessages] = useState([]);

    /**
     * Push a new message to array of messages
     * @param {string} text
     */
    const addMessage = (text) => {
        setMessages([
            ...messages,
            text,
        ])
    }

    const queryChatbot = () => {
        // Add input message / question as new message
        addMessage(inputMessage);

        fetch('/api/chatbot', {
            method: 'post',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                inputMessage,
            }),
        })
            .then(response => response.json())
            .then(data => {
                if (data.response) {
                    // For successful response: Add response as new message
                    addMessage(data.response.output.response);
                } else {
                    // Otherwise add error as new message
                    addMessage('Sorry, no response possible.');
                }
            })
            .catch(error => {
                // In error case we add a new message with an error
                addMessage('Sorry, an error ocurred.');
            });
    }

    return <>
        <input type="text" placeholder="Send a message" onChange={(event) => setInputMessage(event.target.value)} />
        <button onClick={queryChatbot}>Send message</button>
        {messages.map(message => (
            <p>{ message }</p>
        ))}
    </>
}
matthib
  • 11
  • 2
  • Should be `setMessages(messages => [ ...messages, text ])` – Konrad Jul 08 '23 at 10:41
  • Thank you for your answer! In fact, the correct thing to use in this case is the setState callback (react). Better described here: https://stackoverflow.com/questions/42038590/when-to-use-react-setstate-callback – matthib Jul 08 '23 at 18:47
  • It doesn't work in function components – Konrad Jul 08 '23 at 18:50
  • Right, if you use a function component with `useState()` you cannot access/pass the `setState()` callback the same way. But you can pass a function that calculates the next state based on the previous one (like in your example). So this way you can be sure to calculate the correct state because you are mutating the state based only on the previous state, which gets passed in when the function is evaluated. Great read regarding this topic: https://react.dev/learn/queueing-a-series-of-state-updates – matthib Jul 10 '23 at 13:01

0 Answers0