A collection of principles I think are important when building things that live in a browser. Getting more organized all the time.
If you're working on the backend, I also have a lot of ideas about The Elements of APIs.
The Goal
We are building a website or a web application. We want it to work well and achieve the goals of our stakeholders, or company, or project. It should be stable and efficient. Our users' experience should be pleasant if not delightful. Our experience, and the experience of our fellow developers, should also be pleasant. Maintenance and the addition of new features over time should be straightforward and unsurprising.
Toil, like updating dependencies and migrating frameworks, should be minimal. We should be able to step away from the project for some reasonable period of time without anything breaking, and we should be able to come back after some reasonable period of time to resume work and not have to spend much time remembering how everything works. Additional developers should be able to join us in working on our web thing without having to learn many new technologies or ideas.
Approach
- Think about your website (application, etc.) as a unified system, rather than as a series of pages or components. Many of the changes you make could impact large swathes of functionality. This can be scary, and your instinct may be to avoid the possibility via encapsulation. However, encapsulation can lead to inconsistencies and reimplementation, which can mean a worse UX, more difficult maintenance, and more bugs.
- Apply the rule of least power: If you can do it in HTML, do it in HTML. Add the least CSS you can, and cover as much functionality as you can with CSS. Use Javascript as a last resort, and only for non-essential interactions.[^1]
- Similarly, start broad and then work downwards. If your system is suitably generic and flexible, then most of your work will be in tending to the exceptions that arise between different views. This means a rigorous baseline stylesheet, but it also means a mechanism for fetching data that can automatically do the right thing most of the time. (It helps if your website's routes and your API's routes have some kind of clear relationship.)
- Keep things shallow! Your DOM tree, your CSS selectors, your Javascript dependencies, your data structure, your directory structure—avoid organizing through hierarchy. One
<Layout>
component is good; five components like <WrapperTemplate>
are not. You might find Atomic Design to be a useful mental framework, but you don't need a folder in your codebase called "molecules."
- Remember that what ends up in your user's browser is HTML, CSS, and Javascript—not JSX, not SCSS, not Typescript. When developing, that's what's in your browser, too. The more your work in progress looks like the end result, the fewer surprises you'll have at the end and throughout.
[^1]: This is my spiciest opinion, and even if you disagree with this point, the other parts of this guide apply to Jamstack applications too. But please, take some time to think about this one.
HTML and CSS
- Keep your HTML shallow. Every element should either serve a functional/accessible purpose or exist as a hook for styles (in React, make diligent use of
React.Fragment
instead of wrapper <div>
s)
- Establish a baseline of styles that target HTML elements directly such that a new page in your app with no new CSS applied does not look obviously broken—this generally just means getting your typography right, but that goes a long way.
- Learn flexbox and CSS grid; avoid absolute positioning and
margin
. Elements should generally have one of two purposes: presenting content or inviting interaction, or containing other elements. Content/interaction elements shouldn't decide their own layout, but should just stretch to whatever size is available to them; containers dictate what size is available.
- Avoid styling techniques like CSS-in-JS or utility classes. These techniques prioritize element specificity, which is better left as a last resort.
- It can be unnerving to make changes to CSS that may introduce unintended consequences. Consider introducing a visual regression tool like Percy to automate detection of changes to the pages of your site.
Javascript
- Learn about what you can do with the web platform without Javascript—things like the
<details>
tag, CSS grid, the :focus-within
selector—and get in the habit of reaching for those techniques first. The less Javascript your write, the fewer bugs you'll encounter.
- Avoid introducing state. The state of your page should be determined by the URL; its details should be interpreted at the highest possible level and passed down. Some native HTML elements, like
<input>
and <details>
, have their own low-level state that you can use and access without needing to track it. Take advantage of this!
- When you do introduce state, try to store it in the DOM. This often means adding or removing classes (if your state has a visual representation) or
data-*
attributes (if it doesn't). Avoid referencing long-lived variables.
- Don't over-optimize for reuse. If a function is only used in one file, put it in that file. If the function is short, consider inlining its logic in the place where it's called instead. If some other file needs to reference that logic it can be encapsulated and moved or exported in the future.
Data and Props
- URLs are extremely important. The URL in your address bar should relate directly to what's on the page. If something changes on the page, the URL should change. If you refresh, the page shouldn't change.
- Data structures between your API and your site should be identical. Avoid redefining complex objects after receiving them from your API.
- If you must manipulate data, do it as soon as you receive it, then pass it to child components. Child components should not transform data.
- Fetch data as high up the component hierarchy as you can, then pass it around as necessary. Child components' data should be given, not taken.
- Assign callbacks to child components instead of allowing them to access global hooks, especially when the values they're passing or manipulating are complex objects. Child components should tell their parents when their data changes; the parent can then handle all state transitions on its own, in one place.
- State changes should mirror user input closely. Don't transform data until it's time to send it to the server.
- Component trees should be shallow. Ten large components that each control the total lifecycle of one single page is better than twenty medium-sized components that each control a small part of the lifecycle of a few pages.
- Avoid building complex or overly-specific objects and passing them between functions and components. The majority of objects (and thus the majority of defined types, if you're using Typescript) should correspond to persisted entities—that is, database models and GraphQL nodes. Pass resource IDs instead of the full resource object when you can.