At the time of writing, I’ve just finished more than one month at my first job as a beginning React software engineer. During this month, I started out with looking into bugs and addressing them to get myself familiar with the codebase. After shipping a few successful PRs (🎉), my comfortability with the codebase grew, together with my eagerness to be more effective.
During my next task, I had to implement an existing feature in another part of the company’s platform with slightly different configurations. As confident as I now was, I didn’t immediately start out with writing code but rather decided to take my time in setting up an effective plan: Logic wise, the new feature was almost identical to the existing one. The retrieved data was the same, but the result of user interactivity was slightly different. The main differences were UI wise, namely different styling of the existing content, one element of content being added, and a slightly different layout.
After recognising these similarities and differences, my plan of approach was to come up with a clever implementation that would abstract all the similar logic and UI away and open up an API to cover the differences in configurations. Additionally, I would do this in such a way that future cases would also be covered. One of the core concepts of React is reusability, which is one of the reasons that patterns like Higher Order Components (HOC), Render Props, and Hooks exist and are so heavily discussed. And as someone that labels himself as a React engineer, it should make sense to apply these concepts in practice, right?
I started this arguably trivial task at Monday and took my time thinking about potential future use cases and edge cases so that my abstraction would be flawless. Fast forward to Wednesday afternoon, and I still hadn’t started implementing anything yet because I was still deciding on the perfect implementation.
In our team, we have a team meeting at the end of every week in which we reflect upon the past week. A sudden feeling of anxiety and embarrassment as I imagined myself explaining that I basically didn’t do anything this week except for only thinking about an optimal abstraction. Although the team is quite flexible and open about productivity and performance, I was still not too confident about my position in the team and would feel extremely ashamed to tell that this is all I had done this week. Although in the end no big deal was made out of it, I wasn’t quite happy with myself. Particularly, there were a few important lessons that I learned:
- Don’t start out with refactoring. 🕛 What I basically did wrong in this situation is to refactor prematurely (way) ahead of time. It’s not necessarily a bad idea to first analyse and think about your task at hand, rather the contrary, but there’s a limit to it. In my case, I had already looked into the differences and similarities between the two implementations and had an image of how I wanted to abstract the latter. In hindsight, this was already sufficient preparation to get me started on the task. But instead, I tried to be clever by taking this planning a step further and already think about the implementation details of the abstraction and future use cases. This was particularly optimistic as I hadn’t touched nor seen any of the relevant code and had only one month of prior experience with the code base. What I was doing wasn’t proper planning anymore, but rather premature refactoring.
- Copy pasting isn’t bad. 📠 Part of the reason that I started out with refactoring is basically what I said before. Patterns like Hooks, HOCs, Render Props, and others are thrown around a lot in the React community. All of these share the purpose of abstracting away similar logic or view code for the sake of reusability. On top of that, people often say that you should adhere to the DRY (Don’t Repeat Yourself) principle and that duplicate code is a bad pattern. Although all of these patterns make sense, they should not be the starting point, but rather the end goal. In my case, just copy pasting the existing feature and adjusting it from there would have probably been the more effective way of approaching this task. Although this goes against all of the mentioned principles, it doesn’t mean that this will (and should) be the final solution that gets into production. After I get the feature working, then I can look into cleaning up the code through abstractions of similarities.
- Proper refactoring requires proper knowledge. 🤔 This approach of just getting it to work first and refactoring it later does not only improve the amount of time that is spent on the code, but also it’s resulting quality. As I’ve experienced in this scenario, it is really difficult to perform proper refactoring without having any knowledge on the specific code. It’s also really easy to fall into the pitfall of over-engineering it. For me, I was already keeping future edge cases in mind, without even knowing whether they were applicable and valid.
All in all, this experience has taught me quite a bit about how to be more effective at my job. How I shouldn’t start out with planning out refactoring and optimisations, how this will help me not to over-engineer features, and how I shouldn’t let developers pride (to not copy paste) block me from being effective. Especially when I don’t have any prior experience or knowledge of the code that I will be working with, it doesn’t make sense to try to plan out everything and beyond. The chances of me over-engineering the project, lacking critical information, or spending too much time on planning is quite high. Instead, keeping it simple, starting out with copy and pasting, and leaving refactoring for last will make the whole process more effective. This will also result into higher quality code as I will have more knowledge and experience with the code to work with, and thus will better know what code is meaningful to abstract away.
In the end, code will always be susceptible to changes, as it rightfully should. This can be in ways that you had expected it to change, but often times also happens in unexpected ways. Knowing this, it’s unfeasible to come up with a single solution that will work for all future edge cases. Your code doesn’t have to be perfect, and it won’t, it just has to be good enough to solve your current problem properly.