innerHTML vs append with Event Listeners

Ian Marshall
3 min readApr 12, 2021

When creating my first JavaScript / Rails API application I found myself creating many (simple) methods for the first time, like manipulating the DOM and sending fetch requests to the server. Of all the simple methods that I struggled with, adding event listeners was oddly the most vexing.

I built a chat app with a ruby User class, Chat class, and Message class: users have many chats, chats have many users (specifically, two), and users and chats have many messages. This portion of the chat app I was building includes three main functions — getChats(), which fetches chat objects from the database; renderChats(), which confirms the chat includes the current user and creates the chatMarkup; and addEvents(), which adds an event listener to the chat button. There’s also getMessages(), which adds all of the existing messages to each chat, but it’s not as important in the following example. Here’s my original, buggy code:

function getChats() {   clearChats()   fetch(chatsEndPoint)   .then(response => response.json())   .then(chats => {      chats.data.forEach(chat => {         renderChat(chat);      });   })}
function renderChat(chat){ if ((chat.attributes.sender_id.toString() === currentUserId.toString()) || (chat.attributes.recipient_id.toString() === currentUserId.toString())){ const chatMarkup =` <div class = "chat", id = "chat-${chat.id}"> <h3>Chat between ${chat.attributes.recipient.name} and ${chat.attributes.sender.name}</h3> <div class = "messages"> </div> <form method="post" class = "chat-form-chat-${chat.id}"> <input class="message-compose-area" id = "chat-form-chat-${chat.id}" type="text" name="body"></input> <input type="submit" value="Gab" class="send-message" id= "gab-button-${chat.id}"> </input> </form> </div>`; document.querySelector('.grid-container').innerHTML += chatMarkup; addEvents(chat.id); getMessages(chat); }}

The basic flow of this code is as follows: when the getChats() function is called, a fetch request is made to the server which pulls all of the chats between different users. Next, each of those chat objects is iterated over with the renderChat() function, which sorts each chats that includes the current user, either as a sender or a recipient within the chat, before adding them to the page using .innerHTML += chatMarkup.

Finally, two more functions are called — addEvents(), which creates event listeners for the buttons to send messages in each chat, and getMessages, which sorts the nested messages out of each chat and adds them to each chat window:

function addEvents(id){    document.querySelector(`.chat-form-chat-${id}`).addEventListener("submit", function(e){      e.preventDefault();      const messageInput = document.querySelector(`#chat-form-chat-${id}`).value      postMessage(messageInput, id);   });}

Is it the most efficient build in the history of chat apps? Probably (almost certainly) not, but for the most part, this works — it sorts through data and makes information like chats and messages visible to the user. However, one bizarre bug that came out of this was that the event listener assigned to the “submit” button (sending a chat) would only work on the final chat window added to the div.

The issue was my use of innerHTML +=. This specific method wipes out everything inside the div that I am calling it on, then recreates it along with whatever is being added. So if you’re adding chat windows, it’s helpful to have a clearChats() function. In the case of event listeners, this method is especially destructive: every new eventListener added clears out the ones before it.

One simple solution was to refactor my renderChat() function so that instead of using innerHTML += on the chat markup, I first create a div chatWindow which can then be appended:

function renderChat(chat){   if ((chat.attributes.sender_id.toString() === currentUserId.toString()) || (chat.attributes.recipient_id.toString() === currentUserId.toString())){   const chatWindow = document.createElement("div")   const chatMarkup =`   <div class = "chat", id = "chat-${chat.id}">      <h3>Chat between ${chat.attributes.recipient.name} and ${chat.attributes.sender.name}</h3>      <div class = "messages"> </div>         <form method="post" class = "chat-form-chat-${chat.id}">            <input class="message-compose-area" id = "chat-form-chat-${chat.id}" type="text" name="body"></input>            <input type="submit" value="Gab" class="send-message" id= "gab-button-${chat.id}"></input>         </form>      </div>`;   chatWindow.innerHTML += chatMarkup;   document.querySelector('.grid-container').append(chatWindow);   addEvents(chat.id);   getMessages(chat);   }}

Just by switching the order of first creating a chatWindow div element, then appending chatMarkup to that before appending to the document, the event listeners were able to be added in each loop of the getChats() function.

--

--