Next.js Discord

Discord Forum

React.js useState does not update the UI as expected

Answered
Rex posted this in #help-forum
Open in Discord
RexOP
Hello, I have this problem in react.js where I want the UI to be updated after each setBlocks call, but currently it is updating only after the last call of setBlocks, here is my code:
const handleQuery = async (formData: FormData) => {
    const jsonTable = JSON.stringify(table.toJson())
    const query = formData.get("query") as string

    const newBlock = new Block("Query", BlockType.Chat, query)
    setBlocks([...blocks, newBlock])
    formRef.current?.reset()

    const modelResponse = await getAIResponse(query, jsonTable)

    setBlocks([...blocks, new Block("Bot", BlockType.Chat, modelResponse)])
  }
Answered by Ray
just change action props to onSubmit props and it should work
View full answer

47 Replies

Plott Hound
In React, state updates may be asynchronous and batched for performance optimization. This means when you call setBlocks multiple times in quick succession, React may batch these updates together and re-render only once.
Cimarrón Uruguayo
why not just do setBlocks([...blocks, newBlock, modelResponseBlock])?
instead of two setBlocks calls
@Cimarrón Uruguayo why not just do `setBlocks([...blocks, newBlock, modelResponseBlock])`?
RexOP
The UI should be updated two seperate times, the addition of each block should be reflected to the UI
The modelResponseBlock take a long time to be calculated
So the UI should first be updated with the newBlock, then when the modelResponseBlock is ready it should be updated again.
@Rex So the UI should first be updated with the `newBlock`, then when the `modelResponseBlock` is ready it should be updated again.
try this
import { flushSync } from 'react-dom';

const handleQuery = async (formData: FormData) => {
    const jsonTable = JSON.stringify(table.toJson())
    const query = formData.get("query") as string

    const newBlock = new Block("Query", BlockType.Chat, query)
    flushSync(() => {
      setBlocks([...blocks, newBlock])
    }
    formRef.current?.reset()

    const modelResponse = await getAIResponse(query, jsonTable)

    setBlocks([...blocks, new Block("Bot", BlockType.Chat, modelResponse)])
  }
RexOP
OK this does work,
can you explain what flushSync does ?
@Rex OK this does work, can you explain what `flushSync` does ?
it tell react to force update the state instead of waiting the asynchronously
RexOP
Also I see that the docs saying Using flushSync is uncommon and can hurt the performance of your app. so is it the only way to achieve this ?
@Ray yes or change the logic of the code
RexOP
Ok, do have an idea of what changes can be better ?
@Rex Ok, do have an idea of what changes can be better ?
what are you trying to do?
RexOP
So basically this is a chatbot interface, each message is represented by a Block
When the user types a message which is a string, I create a Block object that should then be rendered to the UI,
This message is then sent to an AI engine to generate a response (this takes a long time), after the response is generated, I create Block object from it then display it to the user
@Rex So basically this is a chatbot interface, each message is represented by a Block When the user types a message which is a string, I create a Block object that should then be rendered to the UI, This message is then sent to an AI engine to generate a response (this takes a long time), after the response is generated, I create Block object from it then display it to the user
could you try this
const handleAddMessage =  (formData: FormData) => {
    const jsonTable = JSON.stringify(table.toJson())
    const query = formData.get("query") as string

    const newBlock = new Block("Query", BlockType.Chat, query)
    setBlocks([...blocks, newBlock])
    formRef.current?.reset()

    handleQuery(query,j sonTable)
   
  }

const handleQuery = async () => {
    const modelResponse = await getAIResponse(query, jsonTable)
    setBlocks([...blocks, new Block("Bot", BlockType.Chat, modelResponse)])
}
RexOP
With this, the UI only shows one new block that is coming fromhandleQuery
The first block generated by handleAddMessage is never shown
I tried setBlocks((current) => [...current, newBlock]) but that doesn't fix it
@Rex The first block generated by `handleAddMessage` is never shown
yes use handleAddMessage when the user type message
@Ray yes use `handleAddMessage` when the user type message
RexOP
Yeah I did this
I call it when the user press the submit button
From the browser console I can see that both functions are called at the correct time, but for some reason the UI isn't updating correctly
ok i got it
@Rex Yeah I did this
const handleAddMessage =  (formData: FormData) => {
    const jsonTable = JSON.stringify(table.toJson())
    const query = formData.get("query") as string

    const newBlock = new Block("Query", BlockType.Chat, query)
    setBlocks(b => [...b, newBlock])
    formRef.current?.reset()

    handleQuery(query,j sonTable)
   
  }

const handleQuery = async () => {
    const modelResponse = await getAIResponse(query, jsonTable)
    setBlocks(b => [...b, new Block("Bot", BlockType.Chat, modelResponse)])
}

try again with this
RexOP
Now we are back at the first issue of the UI only be updated after the second call to setBlocks
@Rex Now we are back at the first issue of the UI only be updated after the second call to `setBlocks`
not sure how your rest the code look like but I just tested with this code and it seem to be working
RexOP
Can you send me the code you tested with ?
Plott Hound
I’m away from the pc but would something like this work?

const handleQuery = async (formData: FormData) => {
const jsonTable = JSON.stringify(table.toJson())
const query = formData.get("query") as string

const newBlock = new Block("Query", BlockType.Chat, query)

// First update
setBlocks(prevBlocks => [...prevBlocks, newBlock])

formRef.current?.reset()

const modelResponse = await getAIResponse(query, jsonTable)

// Second update
setBlocks(prevBlocks => [...prevBlocks, new Block("Bot", BlockType.Chat, modelResponse)])
}

Sorry for formatting
@Rex Can you send me the code you tested with ?
import { MouseEvent, useState } from "react";

class Block {
  constructor(id: string, text: string) {
    this.id = id;
    this.text = text;
  }
  id: string;
  text: string;
}

export default function Page() {
  const [blocks, setBlocks] = useState<Block[]>([]);

  const handleClick = async (e: MouseEvent<HTMLButtonElement>) => {
    setBlocks((b) => [...b, new Block(Math.random().toString(), "1")]);

    await new Promise((res) => setTimeout(res, 5000));

    setBlocks((b) => [...b, new Block(Math.random().toString(), "2")]);
  };

  return (
    <div>
      {blocks.map((b) => (
        <div key={b.id}>{b.text}</div>
      ))}
      <button onClick={handleClick}>click</button>
    </div>
  );
}
@Rex Can you send me the code you tested with ?
it works with simple example but I think you page is more complex than this so it might not work as expected
try with flushSync first and see if you have performance issue
RexOP
My page is actually similar to yours, this the the part where I have teh user input, and above it I have the a div that contains the messagaes blocks
<form
                ref={formRef}
                action={handleQuery}
                className="flex gap-x-2 w-3/4 py-4 mt-auto"
              >
                <input
                  name="query"
                  type="text"
                  placeholder="Ask about your data"
                  className=" input input-bordered rounded-full w-full"
                />
                <button
                  disabled={pending}
                  type="submit"
                  className="btn btn-circle text-gray-600 disabled:text-gray-300"
                >
                  <IoSend size={20} />
                </button>
              </form>
RexOP
but your function doesn't look like a server action at all
try put it in onSubmit props
RexOP
But it should work in a way similar to onSubmit
@Rex But it should work in a way similar to `onSubmit`
are you sure lol
it doesn't do anything to me
oh yea it does
it is the exact behaviour you have
just change action props to onSubmit props and it should work
Answer
RexOP
OK this actually solved it.
Thanks 🚀
no prob