NextJS Course (7/9) – Blockbuster Chat V2 – Implementing the Front-End

Welcome back to part 7! Let’s continue straight where we left off and continue on our blockbuster chat V2. Make sure you have your blockbuster_chat_2/page.tsx file open where we just finished defining our ChatBubble component in the last part:

πŸ“ app
    πŸ“ api
        πŸ“ blockbuster
            πŸ“„ route.tsx
        πŸ“ blockbuster_v2
            πŸ“„ route.tsx
    πŸ“ blockbuster_chat
        πŸ“„ page.tsx
    πŸ“ blockbuster_chat_2
        πŸ“„ page.tsx   πŸ› οΈ We'll be working on this file
    πŸ“ counter
        πŸ“„ page.tsx
    πŸ“ types
        πŸ“„ chatMessage.ts
    πŸ“„ favicon.ico
    πŸ“„ globals.css
    πŸ“„ layout.tsx
    πŸ“„ navbar.tsx
    πŸ“„ page.tsx

Getting started on the main component

Let’s write the main BlockbusterChat component in here. This one is going to be pretty large so I’ll necessarily have to break it down into smaller parts with comments in between. This can be a bit confusing indentation-wise so I’ll try to break it up in logical spots and after we are done I’ll show you the completed component once more for clarity.

If at any point the nesting and inner functions get confusing to follow along with check out the video tutorial instead, as it will be easier to follow along and explain visually while we go through this code block. So let’s get to it:

const BlockbusterChat: React.FC = () => {
  const [movieName, setMovieName] = useState<string>("");
  const [movieCharacter, setMovieCharacter] = useState<string>("");
  const [question, setQuestion] = useState<string>("");
  const [isSubmitted, setIsSubmitted] = useState<boolean>(false);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);

This is mostly the same we’ve done before, except the answer state we used last time is now gone and replaced by a new chatHistory state which will be an array [] of ChatMessage objects. Remember this is just the type that we defined ourselves in the types/chatMessage.ts file in the previous part of the tutorial.

Using the useRef hook

Now we’ll write some code to solve a problem that we will encounter. When the chat history gets very long, React will simply add chat bubbles to our page but they will be out of view with a scroll bar appearing as they simply do not fit on the page anymore. The user will have to manually scroll down when the content gets too large for the page. We want the page to scroll down automatically when new messages appear, just like a real chat app, so we’ll have to write some code to make that happen:

  const chatHistoryRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (chatHistoryRef.current) {
      chatHistoryRef.current.scrollTop = chatHistoryRef.current.scrollHeight;
    }
  }, [chatHistory]);

Now this part requires some explanation, but no worries! This is the useEffect hook we learned about in an earlier part. Remember that the code inside the useEffect hook ran once on the first render of the page? This time the dependency array all the way at the tail end of the code contains [chatHistory]. This means that the code inside this useEffect hook will run every time the page’s chatHistory gets updated.

First we use the useRef hook to create a reference to the chatHistory div element. So what is useRef? It allows us to create a reference to an HTML element on the page. The constant chatHistoryRef is a reference to the useRef hook which takes an HTMLDivElement as an argument, but as you can see we default it to null. Later on in the code, when we create the HTML <div> element that will hold the chat history, we will assign a ref to the div so that our code above will know that this is the HTML element we want to target.

If this seems confusing, know that chatHistoryRef, just refers to a div element on our HTML page. Later when we create the div element on our actual page, we will give it a code that matches the chatHistoryRef so that the two will link up together so to speak.

Now the code inside the useEffect hook, which will run every time the chatHistory is updated, will check if the chatHistoryRef.current is not null. This just means that the chatHistoryRef is pointing to an actual HTML element on the page.

If it is not null, we set the scrollTop property of the chatHistoryRef.current to the scrollHeight of the chatHistoryRef.current. This is a bit of a mouthful, but it just sets the current scroll location to the furthest scroll location possible for the chatHistoryRef, which again, we will hook to the specific div element on the page later on, so that this code will apply to that div element that will display the chat bubbles.

Writing the handleSubmit function

Next up we’ll have our handleSubmit function which is going to be reasonably long and is entirely nested within the BlockbusterChat component:

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setIsSubmitted(true);
    setIsLoading(true);

Same as before, we have an async function that takes the FormEvent as input and prevents the default (page refreshing) behavior and we set our isSubmitted and isLoading states to true.

    const questionChatMessage: ChatMessage = {
      text: question,
      sender: "You",
      time_sent: new Date(),
    };

Whenever our form is submitted we create a new ChatMessage object with the question (which is held in React State) as the text. The sender will be You (which we decided in the previous part), and we just set the time_sent to the current date and time.

    setChatHistory((prevChatHistory) => [
      ...prevChatHistory,
      questionChatMessage,
    ]);

We then need to update the ChatHistory state using the setChatHistory function. This one is a bit different as we need to add to the ChatHistory built up so far, so the function takes (prevChatHistory) as input and then returns (=>) a new array that uses the ... spread operator we learned about last time to just put all the previous items that were in the history array into the new one. We then add the questionChatMessage to the end of the array so it is updated with the new question.

Make sure you never use chatHistory.push(questionChatMessage) as this will not work as expected. React will not detect the change in the array and will not re-render the page. Always use the setChatHistory function to update the state and use the ... spread operator to add in the old values.

    const response = await fetch("/api/blockbuster_v2", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        movieName,
        movieCharacter,
        question,
        chatHistory,
      }),
    });

Now we make the call to our new blockbuster_v2 API route. We use the fetch function to make a POST request to the /api/blockbuster_v2 route. This is the same as the other calls we have made to our own APIs before, except this time we add the chatHistory into the body just like we promised to send when we wrote the backend API.

    const data: { answer: string } = await response.json();

We get the data which is a JSON object with a single key answer which is a string (this is just the structure we decided to send back from our API). We parse it and store it in the constant data.

    const answerChatMessage: ChatMessage = {
      text: data.answer,
      sender: movieCharacter,
      isAnswer: true,
      time_sent: new Date(),
    };

We then create a new ChatMessage object to represent this answer chat message. We set the text to the answer we got back from the API, the sender to the movieCharacter (which we have stored in state), and we set isAnswer to true to differentiate it from the question chat messages. We once again set the time_sent to the current date and time.

    setChatHistory((prevChatHistory) => [
      ...prevChatHistory,
      answerChatMessage,
    ]);

    setQuestion("");
    setIsLoading(false);
  };

We update the chatHistory state with the new answerChatMessage using the setChatHistory function just like before, making sure to use the ... spread operator to spread in the old values. We then reset the question state to an empty string as we want the user to be able to ask another question, and set the isLoading state to false.

That completes the handleSubmit function. Just for clarity, we are still inside the BlockbusterChat component:

const BlockbusterChat: React.FC = () => {
  const chatHistoryRef = ...
  useEffect(() => {...}

  const handleSubmit = ...

  // We will be contin