Ch. 62 – Live Chat UI:
Create a “Chat.js” component to show html interface. Copy and paste from the course repo in the “chat-is-visible.html” file. Grab from div with and “id” of “chat-wrapper” down to before the footer. Replace “class” with “className” and “autocomplete” needs to become “autoComplete.”
Keep a piece of state in “Main.js” component to check if the chat is open or not by adding a new “isChatOpen” property set to false in the “initialState” object.
Add a two new cases in the reducer function called “toggleChat” and “closeChat.”
// in the reducer function
case "toggleChat":
draft.isChatOpen = !draft.isChatOpen
return
case "closeChat":
draft.isChatOpen = false
return
Back in “Chat.js” find the “className” of “chat-wrapper–is-visible” and have it be conditionally added based on the state of “appState.isChatOpen.”
className={"chat-wrapper " + (appState.isChatOPen ? "chat-wrapper---is-visible" : "")}
Import “StateContext” and “DispatchContext” at the top of the “Chat” component. Set up constant variables for both “appState” and “appDispatch” set the appropriate useContext().
In “HeaderLoggedIn.js” file we are looking for the “span” element that holds the chat icon in the JSX. Add an “onClick” prop:
<span onClick={() => appDispatch({type: 'toggleChat'})}>
Back in “Chat.js” file, in the JSX, find the “span” element that holds the icon that closes the chat. Add an “onClick:”
<span onClick={() => appDispatch({type: 'closeChat'})}>
So why didn’t we use transition group here? Even if it is not visible we want it to be mounted so we can connect to the chat server.
We want to focus the input field when the chat box is open. Auto-focus wouldn’t work the same way it did for the search because the “Chat” component is not being added to the DOM as it’s already there. So we will use a “useEffect” with an arrow function and “appState.isChatOpen” in the dependency array.
useEffect(() => {
if (appState.isChatOpen) {
// want to focus input field
// doc.querySelector if using Vanilla JS
// need a ref to use
}
}, [appState.isChatOpen])
How we do this is a pretty big question and this is the first time we will need to use “useRef” to imperatively interactive with the DOM.
Note: Remember that React is declarative and Vanilla JS is imperative (explicity).
In JSX, find the input field and give it a “ref” prop named “chatField” and import in “useRef”
const chatField = useRef(null)
useEffect(() => {
if (appState.isChatOpen) {
chatField.current.focus()
}
}, [appState.isChatOpen])
return (
<input ref={chatField} />
)
A ref is like a box that we can hold a value in and unlike state, we are free to directly mutate it. Also, React we not re-render things when your reference changes. “useRef “et’s us take things into our own hands.
Ch. 63 – Sending and Receiving Chats Part 1
In “Chat.js” file, bring in “useImmer” with an initial value of an object with a property of “fieldValue” set to an empty string of text.
Find the input and add an “onChange” prop called “handleFieldChange” to get input value into state. Create the function above the JSX.
On the opening form tag add a “handleSubmit” function to an “onSubmit” prop. Create a function with this name that alerts out the “state.fieldValue”
Have the form automatically clear out for you and we do this by setting “value” prop on the input and set to {state.fieldValue}, essentially pulling from state. This is called a controlled component. In the “handleSubmit” function, after the alert, we use “setState” to set the “fieldValue” back to empty quotes and this is what makes the form input reset to blank or empty.
Let’s change gears and focus on messages displayed.
Store all messages in state and when it changes they will be re-rendered. Set “chatMessages” to an empty array. In “handleSumbit” send message to the chat server and add message to the state collection of messages with message, username and avatar properties:
setState((draft) => {
// Add message to state collection of messages
draft.chatMessages.push({
message: draft.fieldValue,
username: appState.user.username,
avatar: appState.user.avatar,
})
})
In the JSX loop through this array of messages using the map function:
{state.chatMessages.map((messages, index) => {
if (message.username == appState.user.username) {
// return JSX if msg is from you
return (
"Some divs rendering {message.message} and {message.avatar}"
)
}
// return JSX if msg is from someone else
return (
"Some chat-other divs rendering {message.message} and {message.avatar}"
)
})}
Ch. 64 – Sending and Receiving Chats Part 2
In the “handleSubmit” function, instead of using “Axios,” we will leverage “socket.io” in the back-end for two-way communication between the browser and the server. Import “io” from “socket.io-client,” (4m weekly downloads).
Set a constant variable “socket” to the URL of your backend, (ours is “http://localhost:8080”)
In function “socket.emit(a, b)” where “a” is a name of an event type (what the back-end is expecting) and “b” is an object with two properties, message and token.
socket.emit("chatFromBrowser", {message: state.fieldValue, token: appState.user.token})
We want the browser to be listening to an event “chatFromServer,” so set up another “useEffect” with an empty dependency array so it only runs the first time this component renders:
useEffect(() => {
socket.on("chatFromServer", message => {
setState(draft => {
draft.chatMessages.push(messages)
})
})
}, [])
Inside have “socket.on(a, b)” where “a” is the event you are listening for (defined in the back-end) and “b” is a function that updates state using draft.
Jump into JSX and find the “chat-other” div and make sure it is dynamic by hollowing out the “src” attribute and replace with “{message.avatar}” also “{message.username}” and “{message.message}.”
Ch. 65 – Finishing Chat
Add a count for unread chat messages with the icon being red instead of white if you have unread messages.
First let’s implement having links in the chat window for avatar and username by importing “Link” from “react-router-dom” and swapping anchor tags with “Link” tags and swapping “hrefs” for “to.”
Update Link destinations to be {message.avatar} and ${message.username}
Note: ${message.username} is in backticks which is why the dollar sign is needed. It is part of a dynamic URL. {message.avatar} is just in a “src” attribute in the JSX so the dollar sign is not needed.
Add horizontal spacing with {” “} trick.
Add “key” props set to {index} to the “chat-self” and chat-other” divs.
Address the scenario when you have enough messages we have vertical scroll and so we we need to be auto-scrolled to the bottom when a new message is sent. Find div in JSX with “chat-log” class and give it a “ref” of “{chatLog}.”
Add “chatLog” prop with “useRef(null)” just inside the “Chat” component function.
Add a third “useEffect” with dependency array of “state.chatMessages:”
useEffect(() => {
chatLog.current.scrollTop = chatLog.current.scrollHeight
}, [state.chatMessages])
Note that “chatLog.current.scrollHeight” is the entire height of the chat window.
Let’s finally work on unread message feature. The state the holds the number of unread messages shouldn’t live in the chat component because it’s needs to be accessed in the header component. So keep it in the “Main.js” component.
Find “initialState” and under “isChatOpen” create a new property called “unreadChatCount” with a default value of zero.
Next go into the reducer that would allow you to increment this value by one and another action that would set it back down to zero.
// in the reducer function
case "incrementUnreadChatCount":
draft.unreadChatCount++
return
case "clearUnreadChatCount":
draft.unreadChatCount = 0
return
Call these two dispatch actions at precisely the right moment. We only want to add to unread messages when the “chat-box” is not open.
Jump back into “Chat.js,” and find the “useEffect” that is keeping an eye on the chat messages. Add an if statement that says if there’s at least one message && The chat is not open, then use app wide dispatch with a property of type set to “incrementUnreadChatCount.”
useEffect(() => {
chatLog.current.scrollTOp = chatLog.current.scrollHeight
if (state.chatMessages.length && !appState.isChatOpen) {
appDispatch({type: "incrementUnreadChatCount"})
}
}, [state.chatMessages])
To handle the clearing of unread messages when the chat log gets open leverage the “useEffect” keeping tab on “isChatOpen” and call “app-wide dispatch” to dispatch an action that clears the unread chat count.
useEffect(() => {
if (appState.isChatOpen) {
chatField.current.focus()
appDispatch({type: "clearUnreadChatCount"})
}
}, [appState.isChatOpen])
At this point we are updating the state at the correct moments, we just need to update the JSX in the “HeaderLoggedIn.js” file.
Add a conditional className of “text-danger” instead of “text-white”
// in span with onClick={() => appDispatch({type: "toggleChat"})}
className={"mr-2 header-chat-icon " + (appState.unreadChatCount ? "text-danger" : "text-white")}
For handling the number of unread messages displayed, put the entire “span” with “chat-count-badge” class in a conditional that displays is greater than 1. Also want it to show the actual number if greater than 1 but less than 10 or else displays “9+” since we can’t fit more than two digits.
{appState.unreadChatCount ? <span className="chat-count-badge text-white">{appState.unreadChatCount < 10 ? appState.unreadChatCount : "9+"}</span> : ""}