useRef & useState
3 min read
November 29, 2025
As a developer, I am overdependent on using React hooks. Especially when it comes to handling states in client components, it is unavoidable to come across hooks like useState and useRef. When I transitioned from using vanilla JavaScript to React to build web apps, one of the things I liked about React was handling data at the component level. It made my life easier. Leveraging re-rendering with a useState hook was the cherry on top. I have always used useState whenever I have to manage input data from form fields unless there is a need to integrate libraries like React Hook Form or Zod.
Recently, I have been building a research agent using Next.js as the frontend and FastAPI as the backend. WebSocket is the protocol we have been using to transfer data from the backend to the frontend, even though we are not using bidirectional data transactions. I have been predominantly working on the frontend and am responsible for receiving the chunk data sent by the FastAPI backend and streaming it on the page.
I didn't use any external state management tools like Redux or Zustand as I thought the native React Context API would be a good fit for it. Also, I am very comfortable working with useContext. I couldn't leverage utilities like event stream or useChat from ai-sdk as FastAPI uses a different approach to communicate with the frontend. All I did was make a connection with the WebSocket from the connection URL and listen to a bunch of methods to perform different operations. Methods like performing authentication and receiving the JWT token had unique types. Based on the operation, I had to make either a request or response. It was pretty straightforward.
As the number of methods increased, the list of useState hooks also increased. There are other workarounds to reduce the number of useState hooks, but I kept them as they were since I thought it wouldn't be a big deal. But the real problem kicked in when the entire component started re-rendering whenever the WebSocket brought chunk text data from the backend. I double-checked whether the connection was made twice during re-rendering, but it wasn't, as I could check the WebSocket response from the developer console. Each response was coherent and streamed well without any chaos.
I could see the same result when I commented out the useState responsible for pushing recent responses to previous ones. I spent the next four hours trying to fix this behavior. Unfortunately, I couldn't find why it was re-rendering even though I double-checked the dependencies pertaining to connecting the WebSocket. Then I asked myself, "Do you need the component to re-render to perform certain operations?" No. All I did was replace useState with useRef to store all recent responses received from the backend. For some reason, re-rendering had been an unresolvable behavior from my side. So I changed the behavior of retaining the data in the component to work with other methods without any issues.
Coming up with a solution that serves the purpose sometimes doesn't follow a typical workflow. Thinking from the lens of what we want to resolve is very important. It drives us to explore other potential solutions. It is not at all possible without understanding our actual expectations.