loading...

Thoughts about scalable and maintainable frontend architectures

stearm profile image Stefano Armenes ・6 min read

When we speak about scalable architectures we usually refer to pieces of software capable of staying alive and working efficiently also under heavy resource usage. For example, our software has to work in the same way either when used by a few people or millions. Some of the concepts we can hear about are vertical and horizontal scaling, caching, load balancing, batching and asynchronous processing.

The above topics sound like DevOps stuff, do you agree?
As software engineers, how can we contribute to the development of scalable software?

I think that within software development scope, scalable term is pretty much similar to maintainable as well. In some cases, we can interchange usage of these two words, as if they were like the two sides of a coin. Shortly I'm going to explain why.

Imagine that your customer asks for a really important feature, requiring a huge refactor and a massive edit of your codebase. If your code was written to be easily edited also in these kinds of situations, we can say that it is maintainable over time. This wonderful feature that you are going to write would enable the usage of the software to a new slice of users, that didn't consider your software before.
Congratulations, your software is scaled both in terms of feature size and user usage!

In this post, I would like to focus on user interface development. Which kind of tools and which approach should we use to build scalable and maintainable UIs (mostly single-page applications) over time?

Components state design

A core aspect in a modern frontend application is state management. How many times you started designing the shape of your components state and thought: "Cool, easier than I expected" and after a while, your codebase grew up and now you have to edit your previous well-planned state and callbacks to update it when you say "every time the same, it was so simple yesterday". I think that it will always be like this, but with the help of some kind of stuff like React context, useReducer hook, redux (redux toolkit), mobx, etc maybe you can better manage this kind of situations, but use it only when you really need it. Personally, since I use GraphQL I feel very comfortable using the react context and apollo cache. Since the hooks have been released, code is also more readable and elegant.

Component as atomic unit of development

It is convenient to write a component as an atomic unit, without necessarily use it inside your work in progress application. I suggest you have a look at storybook.

In my opinion, writing components through storybook can give you some advantages, for example, you can focus on a single component without being influenced by the layout in which the component will be inserted or interact with your components easily inside different viewports, using different props to test corner cases inside an isolated environment.
Storybook ships with a nice UI through which is possible to explore components, allowing new team members to be familiar with application in a faster way which results in a great team scalability πŸ‘€ β†’ πŸ‘₯ β†’ πŸ‘₯πŸ‘₯ β†’ πŸŽ‰.

Testing

What is the best way to make sure that your software will still be working after adding, deleting or updating your code? Of course by assuring good testing coverage into your app!
But in frontend development is a little different and in my opinion, nicer. I strongly suggest you read this great article by Stefano Magni about frontend testing.

Styling

I adopted the css-in-js approach. I feel really comfortable using styled-components and honestly, I often prefer to duplicate a css instead of over abstracting it.
You know, to avoid a situation like this one:

alt text

Immutability and "fp" style

Forgive me, functional programmers: I'm not speaking about monads, even though I really enjoy it in my little Scala experience.
What I can say is that an immutable approach together with some precautions can help you to write bugless code. For example:

Updating objects using plain javascript can be a little annoying sometimes but you can use helper libraries like immer or immutable.

Benefits

  • memoization
  • code is easier to test
  • you can detect changes using shallow comparison (compare references to objects, not values), which is faster πŸ˜„.

What about pure functions and memoization?

A pure function is a function that has the same return value for the same arguments and not causes side effects... and so, what?
If you are 100% sure that the function f with x as an argument will return y each time you call you can cache result, this is what is called memoization.

As you can imagine, memoization is used also in React to optimize components rendering, take a look at this nice blog post.

ES/TSlint and Prettier

It is always good to have this kind of tool installed to give some standards to the team, restrictions and coding style. Personally, since I use TypeScript I feel the need for linter less.

Types

Last but not last: typed code. Actually I think that it is the most important thing to achieve a good level of code scalability. Typed code allows you to focus on things that really matter and don't care about stuff like "I need to check if this function is called with correct parameters" and consequently you'll write fewer tests.
Typed code is also really helpful and can save you when you have to refactor big projects and it is easy to adopt, incrementally.

Benefits

  • drastic decrease in runtime errors
  • code will be much readable, in this way new people can easily join the team and be productive β†’ team scalability
  • code is self-documented
  • it drives you to think about models before to start writing that is really helpful to understand if what you thought is for real the right thing
  • IDE helps you: code autocomplete, static control flow analysis...

Here is an example of how types can help you in React.

Javascript version

const Dropdown = ({
  value,
  onChange,
  options,
  label,
  placeholder,
  isSearchable,
  required,
  isClearable,
  disabled,
  style
}) => {
  // your component implementation
};

Typescript version

interface Option {
  value: string;
  label: string;
}

interface Props {
  value: { value: string; label: string } | null;
  onChange: (value: { value: string; label: string }) => void;
  options: Array<Option>;
  label: string;
  placeholder: string;
  isSearchable?: boolean;
  isClearable?: boolean;
  required?: boolean;
  disabled?: boolean;
  style?: React.CSSProperties;
}

export const Dropdown: React.FC<Props> = ({
  value,
  onChange,
  options,
  label,
  placeholder,
  isSearchable,
  required,
  isClearable,
  disabled,
  style
}) => {
  // your component implementation
};

It is clear that the second declaration is much easier to understand: we known each single prop type and also if it is required or not. With types definition, you don't need to go through implementation details to understand the shapes of your data.
You can do it also using React propTypes but through a statically type checker this code does not compile if the component is not used correctly, you won't find out at runtime.

You should consider types as your best friends in software development πŸ₯°.
I chose TypeScript to super powering my frontend apps, but you can have a look also to flow.

Links

In my opinion, these are the main pillars to build high quality, maintainable and scalable frontend applications.
I hope this blog post can help you. Any feedback is really appreciated.

Posted on by:

stearm profile

Stefano Armenes

@stearm

Frontend developer, lifelong learner. In love with React and TypeScript. GraphQL worshipper - ReactJS Milano Co-founder

Discussion

markdown guide