A reminder that side projects can both be fun and productive
Building side projects is a lot of fun. Few feelings rival that of sitting in front of your chosen code editor, your favourite beverage or snack in hand and an idea buzzing around in your head. Being productive with side projects is a challenging thing. Over the last few months I have been hacking away at one and whilst it has improved my understanding of some frontend concepts, I recently realised it was not translating into patterns and knowledge I could implement immediately at work. This is because the project, which is building basic versions of frontend tools such as module bundlers, is not an average day-to-day task. So what was the solution? Start another side project, of course. But it was going to be a side project with a difference. I set myself the goal of trying things I knew I could immediately introduce into the frontend codebase at Limejump.
My team is currently migrating an old Angular 1 application to React. Naturally, this presents opportunities to play around with new things and with Limejump giving developers time in each sprint to learn new things, I used that to start this new side project. Unlike previous projects, I began this one by listing the specific features I wanted to play around along with a reason why. They were:
- React Context (we are using Redux and Redux-Saga to handle state management but I wanted to try handling state without them. I had also read that it was useful for dealing with authenticated and unauthenticated routes, something we needed to do)
- Custom React Hooks (I have used the standard
useEffecthooks but had never written a custom one or looked at hooks in any great depth)
- CSS Animations (I wanted to introduce some micro-animations to improve the user experience)
I settled upon building a search app which lets you find out which energy sources are providing the power to your local area. I found the API on this Github repository of free public APIs. The app was influenced by this GIF of an animation I wanted to copy:
So what did I learn?
The hardest thing was fighting against scope creep. For example, I was tempted to rebuild a basic version of Redux using Context. I scratched those itches by leaving comments explaining the steps I could take next. It sounds simple but when time or specifications are not a constraint, it is very easy to get lost in a rabbit hole of implementing new features. The application does not properly handle all the error or loading states and I had to be comfortable with that because my aim was completion not perfection. If I were to continue working on it, I would play around with React’s new experimental Concurrent Mode as it introduces a new way of thinking about handling different UI states.
Context was introduced to make sharing global state through a component tree easier. In my case, I did not really need it because my app was so simple. Initially, I followed the approach advocated in this blog post and created a higher-order component which allowed me to give any component access to Context without needing to use
<AppContext.Consumer> each time.
But I switched to the
useContext hook since it achieved the same result with less code. The higher order component pattern would be useful in a context where you want to mimic React-Redux's
connect function and add extra functionality to the rendered component.
The animation I was copying allows the user to search for results once they have finished typing, a perfect use case for debouncing. After a bit of Googling I came across https://usehooks.com, which has a list of React hooks for various use cases.
useDebounce hook is straightforward. It takes two arguments:
value, which is the value being debounced, and
delay, the length of time to wait before saving the value. So if the delay was one second, the value would be not saved until one second after the user has stopped typing. By default, the
useEffect hook runs after every re-render and in our case, it will run whenever
value changes (you can read more about this behaviour here). The clean up function runs before the next render and ensures our
setTimeout timer begins when the user has stopped typing. If it was omitted, each time the user typed something, it would trigger an API. We can see in the code below where I used
useDebounce. I've omitted some of the code for brevity:
debouncedSearchTerm only changes when the
setTimeout function in
useDebounce is called.
Async Error Wrapping Pattern
This was the idea I was most excited to explore because it solves a really annoying problem caused by using
try/catch blocks to handle errors in async functions. Typically, an async function is wrapped in a
try/catch block so we catch errors if we await on a promise that rejects or run into any other errors. For example,
But what happens if we return a promise that rejects?
We get a lovely
Uncaught (in promise) Error: I am going to error! message in the console. Awaiting on the returned value like this:
return Promise.reject(new Error('I am going to error!')); would work but it is easy to forget doing that. Also, what happens if you want to await on multiple functions? Maybe you want to perform other tasks using the results of previous asynchronous calls. Your async function can easily become a mixture of
try/catch blocks around each awaited call sprinkled with conditional logic to make sure you only move forward if you get the expected result.
So, what is the solution? This:
An awaited function always returns a promise, so we can take advantage of that in
errorWrapper to return data when the promise resolves or an error when it rejects. Returning both these values in array also gives us much cleaner code via destructuring. If you do not want to create your own
errorWrapper function, you can use this package which does the same thing.
But wait…there is more. What happens if an unexpected error happens outside the wrapper function? Something like this:
Async function always a return a promise but that promise rejects if any uncaught errors occur in the function. The solution, believe or not, is a catch block. But it is a catch block at the call-site of the async function.
With the code above, we know that
results will always either be the result of our API call or an error, and so depending on the expected data structure of either outcome, we can do checks against that. In the event that the error is caught in the
results will be undefined and again, we can handle that.
When it comes to prototyping UIs, I always reach for an atomic CSS library such as Tachyons to handle my styling needs. The animation I was copying is simple and in the end was doable using only the transform and transition properties. The tricky part came when I had to transition elements between the different animated states.
Normally, you would create classnames which encapsulate your animation rules and then toggle between them. However, with atomic CSS, each class is responsible for one thing. So, a simple transform like going from
transform: translate3d(0, 40%, 0); to
transform: translate3d(0, 0, 0); is not straightforward. One approach would be to create the relevant single rule classnames and then use the DOM property to read the list of classes applied on the element and then remove and add the appropriate ones. Alternatively, you could achieve the same result by using the
I decided to created classnames which grouped style rules related to animations. I still had my functional classes for rules commonly used between both states such as
transition-duration. I think this approach was a happy medium because it meant I could still easily reason about my styles.
Aside from animations, the most obvious issue with atomic CSS is the unwieldy class names. For example, my input component looks like this:
Definitely an acquired taste.
trans-o-placeholder trans-duration-placeholder trans-duration are some of the custom classes I created whilst
search-input encapsulated multiple rules.
On the flip side, in addition to the speed of UI development, one of the benefits of atomic CSS is the amount of CSS you write. Typically, as you add more UI elements and create new interfaces, you will always need to write CSS, even if you are re-using UI components. With a library such as Tachyons, however, your CSS will not grow as quickly because you are picking classes and composing them. Any CSS you do write will be for specific cases and when it is time to refactor, it is much easier. This topic is actually discussed in more depth on this recent episode of the Full Stack Radio podcast by an engineer from Heroku where they have built a design system using the atomic CSS approach.
None of the ideas expressed in this post are new but it does not hurt to remind ourselves about these things from to time to time. One of the keys to having productive side projects is setting a clear end goal and then literally walking away when finished, even if there are gaping holes in the app. So with that in mind, I would set the following things as candidates for future experimentation:
- Concurrent Mode
- CSS Grid (I used Flexbox to create the UI for displaying the different power sources. It would be interesting to see how the same layout can be achieved with Grid)
- Custom useFetch hook (What opportunities and drawbacks are created by encapsulating the API fetching logic in a hook?)
- Tailwind CSS (this library is probably the most popular implementation of the atomic CSS approach)