ClockUp

#frontend#tech

Introducing yet another random tech product no one except myself will use: ClockUp!

tl;dr - ClockUp is a debloated client web app that consumes the ClickUp API to keep ticket management trivial. Try it out at https://clockup.bossley.com or view the source code.

Motivation

I'm forced to use the ticket management tech product ClickUp for my work and I understandingly have a few gripes against it. ClickUp continues to add features unrelated to ticket writing (what happened to keeping products stupid simple?) and have also fallen victim to the recent AI tech hype[1][2][3].

While I disagree with their product decisions, I can begrudgingly accept them. The main grievance I have against ClickUp, however, is not their product - it's the performance of their product. When I browse their product with Firefox, the site gradually becomes slower and slower until it becomes unusable. This is either due to some broken caching mechanism, a memory leak, or due to the sheer amount of data being fetched on every request. Below is a payload comparison between ClickUp and ClockUp using a hard navigation (without cache) to the same ticket:

ClickUp downloads 114.20 MB
ClockUp downloads 215.64 kB

The only temporary remedy I've discovered is to close my browser and clear all cookies and cache data. For a text ticket writing software, this is unacceptable. As a fellow web engineer, we need do better for the products real people use.

As a stop-gap solution for these performance issues, I decided to put together a simple web application consuming ClickUp's public API to make ticket writing a faster experience for me. I named it ClockUp to indicate the amount of time I will save by using a faster interface. I prefer to spend more time coding than staring at page loading screens and laggy textboxes.

Architecture

ClockUp comprises of exactly 4 routes:

  • login - designated for unauthenticated users logging in or viewing the product for the first time
  • home - displays all ClickUp spaces
  • list - displays an individual list
  • task - displays individual tasks for viewing, editing, and updating

All routes share a main.js script to handle common functionality and a main.css stylesheet for all styles. The main script contains de-duplicated functions and any authentication redirects. Each individual page may contain its own unique script and styles.

There's no build step or minification - in fact, all I do is zip the src directory and upload it to the server. This project is simple enough that I don't find it necessary to implement either.

I also chose not to implement any server-side rendering. Although users will experience a brief loading state before client JS is executed, it makes the overall interface feel quicker with the additional benefit of being able to run this application locally without a server.

All authentication and static user information is stored in local storage with some persistent API data cached in session storage. This makes the application more efficient by not requiring re-authentication on every page load.

Learnings

I love working on minimal projects like ClockUp because I tend to learn a ton of neat web development tricks along the way.

Datalists

Did you know that you can create a pure HTML autocomplete input? HTML5 now has a <datalist> tag which can contain a list of various data. When paired with an <input>, it becomes an input with built-in autocomplete. See it live in action below:

<label>
  Choose your favorite color:
  <input list="list_id">
</label>
<datalist id="list_id">
  <option>Red</option>
  <option>Yellow</option>
  <option>Orange</option>
  <option>Green</option>
  <option>Blue</option>
</datalist>

Note that this doesn't prevent arbitrary input values outside the data list from being submitted, but it provides guidance for users selecting from a list of options. The text-based data lists have been supported by all major browsers since Safari 12.1.

append() vs appendChild()

I've always historically used appendChild() when adding elements to the DOM, but it turns out that append() exists and provides a much simpler API. With append you can append multiple elements simultaneously:

const child1 = document.createElement("span")
const child2 = document.createElement("span")
const child3 = document.createElement("span")

document.appendChild(child1)
document.appendChild(child2)
document.appendChild(child3)

// is equivalent to

document.append(child1, child2, child3)

You can also append text directly without having to explicitly create a text node first:

const text = document.createTextNode("hello")
parent.appendChild(text)

// is equivalent to

parent.append("hello")

Session Storage

I've been on a session storage high lately because it's a fantastic mechanism for storing cache data. It's perfect for cases where you want to persist API data to reduce redundant fetch requests but you won't be concerned if the fetch does happen. Session storage only persists for each "session" (think of an open tab as a session) so it's a great way to increase application performance at virtually no cost.

async function fetchData() {
  const data = JSON.parse(sessionStorage.getItem("my_data"));
  if (data) return data;
  const apiData = await fetch("https://api.example.com/data").then((res) => res.json());
  sessionStorage.setItem("my_data", JSON.stringify(apiData));
  return apiData;
}

Script Execution Order

In HTML5, classic scripts (scripts that are not specified as type="module") run in a blocking sequential order. As a result, it's possible to persist global data between scripts (even if they are not inline).

<script>
  const x = "Hello"
</script>
<script>
  const y = "world!"
</script>
<script>
  const z = x + ", " + y // "Hello, world!"
</script>

If we want these scripts to be fetched asynchronously but still execute in sequential order, we can add the defer attribute:

<script defer src="first.js"></script>
<script defer src="second.js"></script>
<script defer src="third.js"></script>

See V8's explanation of script execution for more details.

Good Public APIs

I've learned about what makes a good public API from my obstacles trying to consume ClickUp's bad public API.

First, only include the minimum data necessary in a response. This minimizes response time and effectively increases throughput. This is why technology like GraphQL exists. ClickUp's API does not do this - they provide extremely large responses for most requests. For example, a folder request provides all folders within a space as well as their subsequent list objects even if that data is unnecessary. In my experience, this results in a heavy 10-12 kB response payload for every request.

Similarly, distinct portions of data should be separated into their own APIs. This allows a consumer to request exactly what they need. To retrieve the available statuses for a given list, I was forced to fetch an entire list of data even if the result I wanted was a static array of strings.

Second, "dogfood" all public APIs. This means that ClickUp should use their own public APIs within their application to ensure the APIs are usable and maintained constantly. There were many times that I found the API documentation to have outdated or misleading information. For example, to reset a task priority to the default value, the documentation does not mention the payload must use null, not 0. I was forced to discover the correct APIs through trial and error for nearly every endpoint.

Finally, include version numbers prefixes in API endpoints. A version number allows APIs to efficiently deprecate old fields without affecting existing functionality. The ClickUp API mentions differences between v2 and v3 endpoints, but all endpoints in their documentation contain both v2 and v3 functionality. This complicated matters when I was deciding whether I should use "folders" or "projects" in the v2 endpoint, or why order indices are included on new endpoints if they are deprecated. If a field is deprecated or renamed, update it in new API versions.

Light and Dark Mode

Modern CSS makes light and dark mode trivial to implement with the light-dark function. It has been available since Safari 17.5.

:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#fff, #000); /* white in light mode, black in dark mode */
}

Conclusion

I'm pleased with the outcome of my project and I plan to heavily rely on it in the coming few months at work. I think the moral of the story is to always strive to make exceptional interfaces that please users. If a tech product causes more frustration than joy for a user, it's worse than having no interface at all.

  1. https://clickup.com/blog/how-to-execute-ai-marketing-campaigns/ ↩︎

  2. https://clickup.com/blog/contextual-ai-why-it-matters-for-the-future-of-work/ ↩︎

  3. https://clickup.com/blog/ai-powered-project-execution/ ↩︎