매일 배운 것을 이해한만큼 정리해봅니다.
새로 옮긴 회사에서 Frontend Mastery에서 발행하는 글을 요약하고 토론하는 스터디를 만들었다.
첫번째 주제는 Building future facing frontend Architectures
, 스터디에서는 미래 지향적인 프론트엔드 아키텍처 구축
로 부르기로 했다. 영어로 주요 내용을 모아봤다.
A deep dive into how component-based frontend architectures can collapse under the weight of complexity at scale, and how to avoid it.
Situation: Frontend projects that are worked on by many developers and teams who use any component-based framework.
Focus: This guide will specifically focus on component code structure that results in resilient front-ends that can easily adapt to changes.
In computer science, a mental model is a way of understanding how a system, software application, or programming language works. It's like a mental picture or framework in your mind that helps you make sense of complex information and solve problems related to that system.
Having a good mental model is important for developers because it helps them design and optimize software more efficiently. It also helps them communicate with other developers by providing a common language and set of concepts to discuss technical issues.
- from chat GPT conversation
A component should ideally only do one thing. If it ends up growing, it should be decomposed into smaller sub components.
// get list from API call somewhere up here
// and then transform into a list we pass to our nav component
const navItems = [
{ label: 'Home', to: '/home' },
{ label: 'Dashboards', to: '/dashboards' },
{ label: 'Settings', to: '/settings' },
]
...
<SideNavigation items={navItems} />
Pros and Cons of Top Down
Pros | Cons |
---|---|
In a rapid development phase for MVP or a small structure application, top down gets a speed and achieves the goal. | Will lose a simplicit: At scale it’s these rapid culmination of these smaller decisions(not changing or trying to think of restructuring, but just adding more props or furcating logics inside of a component) that add up quickly and start to compound the complexity of our components. |
Related APIs need to be corrected when new requirements come in: The problem here is top down components with APIs like this, have to respond to changes in requirements by adding to the API, and forking logic internally based on what is passed in. | |
It becomes a monolithic components that everybody avoids to change: the next developer or team who needs to use or adapt this component is dealing with a monolithic component that requires a complex configuration, that is (let’s be real) most likely poorly documented if at all. |
<SideNavigation>
<Section>
<NavItem to="/home">Home</NavItem>
<NavItem to="/projects">Projects</NavItem>
<Separator />
<NavItem to="/settings">Settings</NavItem>
<LinkItem to="/foo">Foo</NavItem>
</Section>
<NestedGroup>
<NestedSection title="My projects">
<NavItem to="/project-1">Project 1</NavItem>
<NavItem to="/project-2">Project 2</NavItem>
<NavItem to="/project-3">Project 3</NavItem>
<LinkItem to="/foo.com">See documentation</LinkItem>
</NestedSection>
</NestedGroup>
</SideNavigation>
Pros | Cons |
---|---|
creating components whose APIs could be reusable even if they aren’t, generally lead to much more readable, testable, changeable and deletable component structures. | When you’re trying to ship fast this is an unintuitive approach because not every component needs to be reusable in practice. It can be less intuitive at an early stage. |
It’s a more consumable and adaptable long term approach. |
Balancing single responsibility vs DRY.
Thinking bottom up often means embracing composition patterns. Which often means at the points of consumption there can be some duplication. This approach lets you “ride the wave of complexity” as the project grows and requirements change, and allows abstract things for easier consumption at the time it makes sense to.
Inversion of control
In the context of React, we can expose “slots” through children
, or render style props that maintain the inversion of control on the consumers side. <Button />
itself could be composed from smaller primitives under the hood.
// A "top down" approach to a simple button API
<Button isLoading={loading} />
// with inversion of control
// provide a slot consumers can utilize how they see fit
<Button before={loading ? <LoadingSpinner /> : null} />
Open for extension
Ideally your components do one thing. So in the case of a pre-made abstraction, consumers can take that one thing they need and wrap it to extend with their own functionality. Alternatively they can just take a few primitives that make up that existing abstraction and construct what they need.
Leveraging storybook driven development
We can adopt the models behind their thinking when building out our UI components in isolation with storybook and have stories for each type of possible state the component can be in.
Doing it upfront like this can avoid you realizing that in production you forgot to implement a good error state.
loading
and error
props what happens if they are both true
? (In this example it’s probably an opportunity to rethink the component API)isSomething
renderX
, renderY
methods tend to be a smell. These are usually a sign a component is becoming monolithic and is a good candidate for decomposition.The models we have affect the many micro-decisions we make when designing and building frontend components. Making these explicit is useful because they accumulate pretty rapidly. The accumulation of these decisions ultimately determine what becomes possible - either increasing or reducing the friction to add new features or adopt new architectures that allow us to scale further (not sure about this point or merge it below).
Going top down versus bottom up when constructing components can lead to vastly different outcomes at scale. A top down mental model is usually the most intuitive when building components. The most common model when it comes to decomposing UI, is to draw boxes around areas of functionality which then become your components. This process of functional decomposition is top down and often leads to the creation of specialized components with a particular abstraction straight away. Requirements will change. And within a few iterations it’s very easy for these components to rapidly become monolithic components.
Designing and building top down can lead to monolithic components. A codebase full of monolithic components results in an end frontend architecture that is slow and not resilient to change. Monolithic components are bad because:
We can avoid the creation of monolithic components by understanding the underlying models and circumstances that often lead to the creation premature abstractions or the continued extension of them.
React lends itself more effectively to a bottom up model when designing components. This more effectively allows you to avoid premature abstractions. Such that we can “ride the wave of complexity” and abstract when the time is right. Building this way affords more possibilities for component composition patterns to be realized. Being aware of how costly monolithic components truly are, we can apply standard refactoring practices to decompose them regularly as part of everyday product development.