š ļø 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:
- Put everything into
makeErrorElement
- 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
:
- Format the error message
- Make the error element
- 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:
- Check if we should show the empty state
- If yes, show the empty state
- If no, hide the empty state
We can make this easier if we split the code up into three functions:
- A function to check if we should show the empty state
- A function to show the empty state
- 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:
- Does the task list contain any elements?
- 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:
- User deletes a task, but deletion is not successful (see above āļø).
- 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!