There are no ideal software projects. Challenges are plenty, caused by technology and people. But I believe that if you provide a fantastic developer experience, you end up creating a higher quality product. Bugs are a manifestation of lack of knowledge, confusion, short cuts, bad practices, and over-dependence on horizontal organizations/packages. Here are a few factors that can improve a frontend developer’s day in the life. While these factors may seem like common sense, we still see these compromised, especially as a project age.
1. Learning curve
How soon can a new developer start contributing to the project? As a frontend developer, can someone with a good grasp of JavaScript, CSS an HTML get started easily?
Framework
How easy is your framework to master? I like to React because of its small footprint. JSX is just JavaScript expressions that embrace composition. In fact, the composition is the essence of React.
Angular has a huge learning curve, as it forces you to adopt class-based development and learn advanced TypeScript. RX.js has a huge API to master, along with vast framework documentation. I’d use RX.js as a library when the problem domain demands it. UI is not it.
Vue.js seems simple and has a small API footprint. I believe Vue.js is a comfort zone for those coming from the jQuery world who like keeping JS, HTML, and CSS separate. Yet, as a software engineer, I find it tough to reason string-based dependency injection and DSLs.
The composition is the essence of the software.
React is just that. A React App is a component that is composed of smaller components. React started off with class-based components. Now, with the advent of hooks, you can abandon classes completely. (except for Error Boundaries because there is no hook available to match the componentDidCatch lifecycle method).
With this approach, you can also abandon a huge chunk of ES6 and TypeScript that deal with classes. At HPE, our team not only follows this for our React projects, we also take it a step further to trim down our JavaScript.
The learning curve from dependencies
CRA helps convert React to a framework that solves most of the frontend issues such as SASS, build, etc. However, you may need to adopt a few more libraries such as:
- A component library
- Routing
- i18n
- Form management
- State management
IMO React’s useState(), useReducer(), and Context API are plenty enough to manage state in a clean way. Do your due diligence to keep dependencies minimal. Some libraries are magical. Magic is dangerous. We use Formik in our projects and it is fantastic. Magical. That is until you try to organize your forms. Formik’s magic adds a steep learning curve when you need to extend it to custom components. It still beats react’s form binding.
Documentation
Other than high-level architecture and design docs, the only documentation I recommend is live ones. We use Storybook extensively to demonstrate component usage and layout patterns. For more complex use cases, we create full-page samples that demonstrate state management and API integration. Nothing like copy/paste-able working code to speed up your development.
In every project that I have worked on, the docs that live on an internal wiki site become stale after a while. No one has the drive to keep it fresh. No one tracks it. No one misses it.
2. Hit the road running
How soon can a newly on-boarded developer see a working version of the app on his laptop? My goal is this:
- Install git, node, nvm and vscode
- Clone the repo
- npm install
- Run a couple of cli-s if need to.
- npm start
But for most projects I’ve worked on in the past, this is not the case. Here are certain things I’ve encountered.
- I follow a series of steps from a wiki page and hit a roadblock. Now, someone has to send me instructions over email on the extra steps needed. This is called tribal knowledge. It is very common. It is created via quirks in OS dependency, short cuts are done to make the app work, etc.
- I set up and run databases and several APIs locally. These steps are not documented properly and often documentation is stale. My only hope is to find someone who has walked this path before to help me.
- To get some parts of the software running, I have to install Eclipse, modify the environment variable, and run Java servers. For this to work, someone definitely needs to send you a list of environment variables. For those without Java experience, this is excruciating.
What can you do:
- Create mock APIs that can run without database or server dependencies. Run it right within CRA. Use a library-like faker to generate data. Keep the APIs as close as possible to the real ones. This also helps to make progress even when the APIs are not ready. If coded to the exact specs, integration is a breeze.
- Be aware of tribal knowledge. It is the information that resides in an obscure stale wiki, old emails, bashrc files, or chat. When you need to take extra steps for your app to run, build that into the task runner.
- Make it easy for new developers to get started quickly and incrementally learn. No one should have to go through months of training before getting started. Just JavaScript expertise should be plenty enough.
Keep it Simple
Easy to say. Hard to accomplish.
What can be avoided is accidental complexity. This arises from the technology choices and design strategies we make while solving the domain’s problem. As mentioned before, for frontend development, your framework and library choices matter. Here are a few things you can do to tone down accidental complexity:
- Adopt a simple language. For us, JavaScript is the only choice, but you can further simplify it as mentioned before.
- Adopt frameworks and libraries with a small API footprint. No matter how appealing the tagline is.. Such as “everything is a stream” (Rx.js), it may only add more complexity in most cases.
- Choose technologies that can be adopted incrementally. We use TypeScript extensively in our projects. Yet, it wasn’t the case when we started. Until we understood it fully and embraced good patterns, it was optional. The learning was incremental. Even now, for Higher-Order functions and components, we find it tough to type them fully. Here, instead of tacking on complex typing, we simply use “any” type and the unit tests take care of their resiliency.
- Avoid OOP. OOP was not used or implemented how it was meant to be. For front end engineers, J2EE completely screwed it up with EJBs and such. Then there was a push to simplify it with the POJO movement. I have been in projects where a simple project designed by an architect had posted a full wall of UML diagram, explaining his design. Java may be great for the server-side but was a horrible choice for frontend. GWT anyone?
- Even in the past decade where the power of JavaScript as a functional language was revealed, the TC39 ECMA foundation keeps pushing half-baked class paradigms into JavaScript.
- Use composition as the essence of your design. Avoid class-based inheritance. Use functional patterns such as compose, map/filter, and reduce for data transformations and state management. Use a library such as ramda.js to provide object and list management utilities.
Programming practices
We talked about taking a minimalist approach to JavaScript and adopting functional patterns. Sticking to the following practices can keep the code less buggy and developer experience better:
Practice immutability
- Prefer const over let. This is the first level of immutability that is effective for non-object types.
- Use spread operator to create new copies instead of mutating an existing object. Use deep cloning if you need to.
- Use reducers/actions for state management. A reducer used for managing state is a pure function that, given the current state and an action, returns a new state.
- Learn how to write pure functions. Do not mutate function arguments or anything outside its scope. Learn how to unit test pure functions well.
Write idiomatic code
- Some developers get high writing clever code, and I was one of them. Clever code is just that. Clever. It is tough to reason about. Avoid clever code and call it out when you see them in code reviews.
- Use a linter and integrate it with git hooks. Use an opinionated formatter such as Prettier. Enforce conventions such as variable names and spacing on top of this via code reviews.
Use static typing
TypeScript is intimidating. The documentation pretends that it is its own language. However, we adopted a minimalistic approach to it by abandoning class-based development. After this, TypeScript became our friend. We mostly use TypeScript for:
- Function signatures
- Component Prop Types
- State and local variables
HOCs and dynamic behavior are really tough to type. We shamelessly use “any” type without wasting hours googling on “how to type”. Remember, a TypeScript is just a tool. Don’t treat it like a dogma.
Unit tests
- Have enough demonstrable unit tests. This helps new developers learn how to write assertions for helper functions and components. Jest assertions and snapshots are wonderful.
- If you find yourselves injecting a mock into a unit test, it may be time to move that into an integration or acceptance test. Here is a recommended read.
- Writing software by composing unit testable functions and components is an acquired skill. When you build software composed of resilient units, the overall project will reflect that.
Acceptance tests
- Acceptance tests are integration tests performed within a functional scope. Functional tests are traditionally done by QA using Java-based Selenium. Using frameworks such as cypress and test case, developers can write isolated acceptance tests in JavaScript.
- In our team, QA leverages this furthermore by extending the scenarios in their test suites. This eliminates much of the testing overlap between dev and QA.
Bus factor
The “bus factor” is the minimum number of team members that have to suddenly disappear from a project before the project stalls due to a lack of knowledgeable or competent personnel. In software development, you can minimize this by taking some measures.
- No Heroes. Heroes are amazing engineers who would resolve issues single-handedly without blinking an eye. They are hard workers and experts in the domain and project. However, when a hero leaves a project, it leaves a huge hole. Two weeks of recorded TOI sessions are not going to stop the impact.
- The same point again: minimize the project complexity. Make sure that your project can be developed and maintained by engineers with a wide range of capabilities. You should be able to onboard new developers quickly. If your project requires deep knowledge of stuff, make sure that there are enough people that have that.
Development in isolation
This is a high goal. Ability to write code in isolation means that:
- You understand the problem you are solving clearly.
- You do not have any dependency on anyone. You are not blocked.
- You can be in the flow. Writing and refactoring as you develop.
- Merge conflicts are far and few because you are not sharing that folder with anyone.
- The risk of regression is low.
- You can write a self-contained unit and acceptance tests.
I call this a high goal because it depends on how isolated your features are designed. If every feature in your app needs you to work on common sections such as updating routes, metadata etc, there is constant conflict.
Deleting features
Most projects only increase in size. Deprecated functionality aka dead code lingers forever because of regression risk. Ideally, deleting a feature should be as simple as removing a folder and its routes. This is a great goal to shoot for. It means that you can enhance, refactor, or remove features without regression. You could even do customer tests for two radically different versions of the same feature.
The composition as a micro frontend architecture.
Applets, Flash, and iframes are micro frontends. But all of them are dead (iframes are used as hacks usually). There are a few blogs you can find about SPA micro frontends. The goal is to develop a container that can host loosely coupled pieces of software. It is a worthy quest. The solutions are still very custom and complex because it tries to solve it within a broad scope. This demand usually occurs when different teams want to deploy their apps in the same “portal”. For example, Angular and React apps living side by side in the same browser tab, which introduces unwanted complexity in your project.
We, at HPE (in our team), discussed this. We decided not to shoot for this overarching goal of mixing frameworks but instead aimed for an isolated feature development model to React. There was no need to combine JavaScript frameworks. Our platform became a combination of:
- Core functionality such as npm tasks, storybook, common components, themes and helpers
- A set of isolated features. Global states such as user info, routing, etc. are injected into the features containers.
- App shells that could create different personas based on the platform it was deployed in. A persona would simply create an experience by picking certain features and compose them within an App shell.
The project was front-loaded with #1. After a couple sprints, #2 and #3 were all we did. The developer experience was fantastic. By applying various principles as explained in this blog, we were able to easily scope and assign individual features to developers. Onboarding a JavaScript engineer took less than 2 weeks. In about 5 months, we had churned out over 30 features with a team of 13 frontend engineers. The project went from zero to 1400 typescript files and 80K lines of code, unit, and acceptance tests included. Hurray!
How to keep DX delightful
Keeping DX delightful is a constant battle. Periodic tech sessions and nit-picky code reviews are a must. You have to be aware of the following:
Bad programmers
Like it or not, you will onboard less than ideal programmers into your team. They will write bad code. Especially in the beginning. You will need to encourage them to follow the core principles and push them to write idiomatic code. With isolated feature development, you can mitigate the risk. Bad code can be refactored without regression.
Code Smells
“A code smell is a surface indication that usually corresponds to a deeper problem in the system.” ~ Martin Fowler
JavaScript is a flawed language. It was created in a hurry with constructs added to compete with Java. Code smells are plenty here. For example, in our React projects, using let is a code smell. Imperative data transformations are not allowed. Being aware of code smells is very key. You enforce it as a team during code reviews.
Normalization of Deviation
Googling the term will give you an idea about what it means and how it applies to different domains.
For example, in an ongoing project, suddenly there is a timeline shift and urgency. Unit tests take a back seat because developers are pressed for time. Thus, they approach it as “make it work and leave it to QA”. The backlog is now is normal. There is really no “urgency” to fix it since everything seems ok. But this is a ticking time bomb that will manifest itself in the worst possible way soon.
Even small undesirable deviations such as variable name conventions can get picked up and create nonidiomatic code. It is only a matter of time before someone will try and access document, window, or localStorage variable without discipline. If you’re not able to catch and tame it on time, in a couple years, that practice will spread. Guaranteed.
New dependencies
As a project age, new dependencies will arise. It is important to pick packages that have a large contributor base and a good online presence. Make sure that the packages will seamlessly upgrade with others. Using open source in a fast-moving tech is rife with issues such as is-promise, core-js, left-pad, and event-stream. Frankly, adopting single function packages like is-promise is ridiculous. Is it that tough to write a simple function called isPromise()?
Refactoring
IMO, refactoring should never stop. I am always in a constant quest to simplify the software. Sometimes, it is using downtime to audit your app for code smells and complexity. Sometimes it is as simple as finding a better name for a function or file name. Refactoring is like maintaining your car. Do it regularly, and it will last you a long time. You do not want your product to become a “legacy” app that developers can’t wait to rewrite.
Code Reviews
Code reviews must be a positive experience. Reviews tend to be impersonal because of its textual nature. Comments must be grounded in empathy and politeness and should enforce shared ownership of the codebase. A code review is not a display of cleverness or coding muscles, it is about building a project together. In our team, we encourage everyone to participate in reviews. It helps to keep the best practices going and keep the code idiomatic.
Upgrades
Npm packages become stale within weeks if not months. Frameworks are notorious for introducing breaking changes. Keeping up with stable versions will help keep your product fresh.
Shiny old things
Every now and then, a shiny new solution will rise in the npm horizon. To create products that last long, resist the urge to be an early adopter. Adopt shiny old things.
Evolve
Tools will change. New philosophies will emerge. Frontend solutions are still complex, but the pace of innovation is fast and furious. We have to constantly adapt and evolve.
Zoom’s pledge to work with law enforcement by not offering the strongest encryption for free calls has spurred online blowback, drawing criticism amid #BlackLivesMatters protests about the role of police in the U.S.— Bloomberg QuickTake (@QuickTake) June 9, 2020
More via @business: https://t.co/8UY76wQsJd pic.twitter.com/PRY82wkeMD