šŸ› ļø Todolist: Refactor

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

šŸ› ļø Todolist: Refactor

Itā€™s now a good time to do a refactor since we completed Optimistic UI for the Todolist. Letā€™s go through the code from top to bottom together. Iā€™ll only write the parts that we can refactor.

updateConnectionStatus

We wrote three lines of code to check whether the user is online or offline. The first setConnectionStatus tells us whether the user is offline when they load the site.

The other two lines update the connection status.

setConnectionStatus()
window.addEventListener('online', setConnectionStatus)
window.addEventListener('offline', setConnectionStatus)

Makes sense to group these three lines into a function.

function updateConnectionStatus () {
  setConnectionStatus()
  window.addEventListener('online', setConnectionStatus)
  window.addEventListener('offline', setConnectionStatus)
}

Although this is optional, we can put setConnectionStatus inside updateConnectionStatus. This makes it easier understand the code since setConnectionStatus is quite short anyway.

function updateConnectionStatus () {
  function setConnectionStatus () {
    // ...
  }

  setConnectionStatus()
  window.addEventListener('online', setConnectionStatus)
  window.addEventListener('offline', setConnectionStatus)
}

Using updateConnectionStatus:

updateConnectionStatus()

Simpler error messages

We wrote this for error messages. The first part of the code formats the error message while the second part creates and adds an error element to the DOM.

// Format error message
let errorMessage = ''
const { message } = error.body
if (message === 'TypeError: Failed to fetch') {
  errorMessage = 'Failed to reach server. Please try again later.'
} else if (message === 'Unauthorized') {
  errorMessage = 'Invalid username or password. Please check your username or password.'
} else {
  errorMessage = error.body.message
}

// Create error element
const errorElement = makeErrorElement(errorMessage)

// Add error element to DOM
flashContainer.appendChild(errorElement)

We can group the first part into a function called formatErrorMessage. The function name clarifies what the code does.

const formatErrorMessage = _ => {
  // ...
}

First, we copy-paste the code into formatErrorMessage.

const formatErrorMessage = _ => {
  let errorMessage = ''
  const { message } = error.body
  if (message === 'TypeError: Failed to fetch') {
    errorMessage = 'Failed to reach server. Please try again later.'
  } else if (message === 'Unauthorized') {
    errorMessage = 'Invalid username or password. Please check your username or password.'
  } else {
    errorMessage = error.body.message
  }
}

Here, you can see we need a message from error.body. We can pass the message into this function directly.

const formatErrorMessage = message => {
  let errorMessage = ''
  if (message === 'TypeError: Failed to fetch') {
    errorMessage = 'Failed to reach server. Please try again later.'
  } else if (message === 'Unauthorized') {
    errorMessage = 'Invalid username or password. Please check your username or password.'
  } else {
    errorMessage = message
  }
}

We can also use early return statements to simplify the function.

const formatErrorMessage = message => {
  if (message === 'TypeError: Failed to fetch') {
    return 'Failed to reach server. Please try again later.'
  }

  if (message === 'Unauthorized') {
    return 'Invalid username or password. Please check your username or password.'
  }

  return message
}

Now the block of code for creating error messages become this:

// Format error message
const errorMessage = formatErrorMessage(error.body.message)

// Create error element
const errorElement = makeErrorElement(errorMessage)

// Add error element to DOM
flashContainer.appendChild(errorElement)

We can group these three lines of code into one function so we donā€™t have to write them multiple times.

We have a choice here. We can either:

  1. Put everything into makeErrorElement
  2. Create a new function to wrap these three lines.

Both make sense. For me, I would put everything into makeErrorElement.

const makeErrorElement = message => {
  // Formats the error message first
  message = formatErrorMessage(message)

  // Create the error element
  const errorElement = document.createElement('div')
  errorElement.classList.add('flash')
  errorElement.dataset.type = 'error'
  errorElement.innerHTML = /*...*/

  // Append element to the DOM
  flashContainer.appendChild(errorElement)
}

With this, we do three things in makeErrorElement:

  1. Format the error message
  2. Make the error element
  3. Add the error element to the DOM

The name ā€œmakeErrorElementā€ doesnā€™t make sense anymore since itā€™s one of the three things. A better name would be createErrorMessage.

const createErrorMessage = message => {
  // Formats the error message first
  message = formatErrorMessage(message)

  // Create the error element
  const errorElement = document.createElement('div')
  errorElement.classList.add('flash')
  errorElement.dataset.type = 'error'
  errorElement.innerHTML = /*...*/

  // Append element to the DOM
  flashContainer.appendChild(errorElement)
}

You can now use createErrorMessage in every catch statement.

zlFetch(/*...*/)
  .then(/*...*/)
  .catch(error => {
    // ...
    createError(error.body.message)
  })

Showing and hiding tasks

We had to write these three lines of code to show and hide our tasks:

// Show task
taskElement.removeAttribute('hidden')

// Hide task
taskElement.setAttribute('hidden', true)

// Check if task is hidden
taskElement.hasAttribute('hidden')

Weā€™ve done this many times now. Letā€™s create a function for each of these lines of code.

// Show task
function showTask (taskElement) {
  return taskElement.removeAttribute('hidden')
}

// Hide task
function hideTask (taskElement) {
  return taskElement.setAttribute('hidden', true)
}

// Check if task is hidden
function isTaskHidden (taskElement) {
  return taskElement.hasAttribute('hidden')
}

We can make these functions more generic since we always hide things with hidden. They can be used for any element.

// Show element
function showElement (element) {
  return element.removeAttribute('hidden')
}

// Hide element
function hideElement (element) {
  return element.setAttribute('hidden', true)
}

// Check if element is hidden
function isElementHidden (element) {
  return element.hasAttribute('hidden')
}

You should know how to use these by now :)

Simplifying the empty state

We showed the empty state with every and if/else. It looks quite complicated.

// Deleting tasks
taskList.addEventListener('click', event => {
  // ...
  const allTasksAreHidden = [...taskList.children]
    .every(element => {
      return element.hasAttribute('hidden')
    })

  if (allTasksAreHidden) {
    taskList.classList.add('is-empty')
  } else {
    taskList.classList.remove('is-empty')
  }

  // ...
})

What weā€™re doing here is:

  1. Check if we should show the empty state
  2. If yes, show the empty state
  3. If no, hide the empty state

We can make this easier if we split the code up into three functions:

  1. A function to check if we should show the empty state
  2. A function to show the empty state
  3. A function to hide the empty state
const shouldShowEmptyState = _ => { /*...*/ }
const showEmptyState = _ => { /*...*/ }
const hideEmptyState = _ => { /*...*/ }

shouldShowEmptyState

We need to check for two things to decide whether to show the empty state:

  1. Does the task list contain any elements?
  2. Are all tasks hidden from view?

First, we want to show the empty state if taskList contains no elements.

const shouldShowEmptyState = _ => {
  if (taskList.children.length === 0) return true
}

Second, if taskList contains elements, we want to check if theyā€™re all hidden.

const shouldShowEmptyState = _ => {
  if (taskList.children.length === 0) return true
  return [...taskList.children].every(isElementHidden)
}

showEmptyState

We show the empty state by adding .is-empty to taskList.

const showEmptyState = _ => {
  taskList.classList.add('is-empty')
}

We need to check whether we should show the empty state when a user deletes a task. If yes, weā€™ll use showEmptyState to show the empty state.

taskList.addEventListener('click', event => {
  // ...
  hideElement(taskElement)
  if (shouldShowEmptyState()) showEmptyState()

  zlFetch.delete(/*...*)
})

If we get an error from the server while deleting tasks, we need to hide the empty state.

taskList.addEventListener('click', event => {
  // ...

  zlFetch.delete(/*...*/)
    .catch(_ => {
      // ...
      showElement(taskElement)
      hideEmptyState()
    })
})

This brings us to hideEmptyState.

hideEmptyState

We can hide the empty state by removing .is-empty from taskList.

const hideEmptyState = _ => {
  taskList.classList.remove('is-empty')
}

We need to hide the empty state when the following occurs:

  1. User deletes a task, but deletion is not successful (see above ā˜ļø).
  2. User adds a task

Weā€™ve done (1), so letā€™s on (2) now.

We need to hide the empty state when a user adds a task. This is because thereā€™ll always be at least one task in the DOM.

// Adding a task to the DOM
todolist.addEventListener('submit', event => {
  // ...
  taskList.appendChild(tempTaskElement)
  hideEmptyState()
  // ...
})

If the server responds with an error, we need to remove the added task. In this case, we need to check whether we should show the empty state. If yes, we show the empty state with showEmptyState.

// Adding a task to the DOM
todolist.addEventListener('submit', event => {
  // ...
  zlFetch.post(/*...*/)
    .then(/*...*/)
    .catch(error => {
      // ...
      taskList.removeChild(tempTaskElement)
      if (shouldShowEmptyState()) showEmptyState()
    })
})

Thatā€™s it!