This is a project structure style guide
based on lessons learned from developing multiple FE projects using component based architecture and Redux style state management.
For anyone looking for ideas on how to structure their FE projects using frameworks such as Angular, React, Vue.js and Ember.js who already have some understanding of Redux or similar state management patterns.
That value:
- fast development
- scalability and maintainability
- excitement of using new technologies
- ease to work with big teams
- codebase longevity
These can be projects with low or high levels of complexity, this solution works well for all projects.
However if you are new to Component Based Architecture and Redux have in mind that these concepts are not absorbed overnight and that if your project just contains a couple of views it will be certainly faster for you to just hack it together keeping the needed code for each view simply in the respective View/Container Component or use an MVC architecture if you already know it.
It's all about managing code complexity.
Nothing slows development more than having to think about how and where to integrate a new feature.
The FE handles a lot: Markup, Styles, Logic, API Requests, Authentication, Mobile and Screen Size Specific Logic, File Uploads, User Specific Logic, Async Events and Handlers, ... The list is pretty much infinite and varies from project to project.
For years FE has been a nightmare for developers, until the birth of the first FE frameworks many BE developers would refuse to touch the FE and many still do for considering it too unpredictable and chaotic.
This has to do with:
- JS was initially unreliable compared with other languages that were used in the BE and it still has a lot of 'magic' behaviors.
- With the fast paced evolution of browsers and FE communities FE code is constantly becoming obsolete.
- CSS is hard to maintain, conflicts with itself a lot and behaves differently on different browsers and devices.
- Local State is very hard to Scale
-
Architecture helps us not deviate too much from it by giving us guidance, that should be easy and intuitive to follow.
The dream is to develop projects that are instantly understandable by any developer it is handed to and to make it straightforward how to add new features or maintain current features to anyone. This way collaboration is seamless and the amount of people contributing to it can scale smoothly.
This is easily the most complex and hard to achieve characteristic in software development, great to keep this concern in mind like a north star.
-
-
UI Components - HTML, CSS and some UX logic if easy to encapsulate in one component
-
State - All operations Regarding State
Why ? These two concerns are fundamentally different so working with one or the other requires a different mindset making it lighter on the brain to work on one or the other at a time, also organizing them differently makes them easier to work with. UI Components scale better in a deep Component Architecture and State Stores Scale more smoothly in an horizontal file structure (more on this bellow).
-
-
By having predictably encapsulated components and State we can safely use new technologies and Architecture in specific Components or Stores without having to affect other parts of the application.
This is specially important for long term projects and scaling teams as both time and developers will push the project to use new technologies and architectures, depriving them of this is an option, but it will result in massive refactorings where work stops completely while the entire codebase is adapted to something new.
With allowing to try new things in an encapsulated non conflicting way there is no need for refactoring, old stuff stays old and new stuff stays new. All that you need to do is have a README.md for the core architecture and small README.md s for each new alternative at the respective folder level. What can happen is that the core architecture gets switched, in this case all that needs to be updated is still just README.md s, not necessarily the code which is brutally expensive to refactor for companies of any size.
-
Being easy to confidently track all data affecting a component is easy when it comes from a single source that is easy to read and to track changes on.
-
Managing UI can be very hard, most of the difficulty is solved by encapsulation.
A way I've found to keep development going at a fast pace and to keep our project easy to maintain is to not worry too much about re-usability, what's truly important is encapsulation, reusability comes naturally after.
To maintain UI complexity well encapsulated split components into smaller subcomponents that get used by the parent.
UI components react to state and trigger state changes but shouldn't store it locally and don't alter it directly.
All complexity related with state changes should not be in the components, it should be in the State Stores.
However, there are some cases where local state is accepted, even though even these could be avoided. These usually involve inputs with two way data binding, form validation and the need for a quick delivery.
Route-components
and Stores
are allowed to have different architectures and technologies from what's considered to be standard, just include a README.md at in the respective Route-component
or Store
so that other developers quickly spot the differences.
This is a folder structure that solves implements all the solutions announced above.
├─root-file-1
├─root-file-2
│
├─src/
│ │
│ ├─app/
│ │ ├─app-component.[logic|markup|style]
│ │ │
│ │ ├─route-component-1/
│ │ │ ├─route-component-1.[logic|markup|style]
│ │ │ ├─sub-component-1/
│ │ │ │ └─sub-component-1.[logic|markup|style]
│ │ │ │ └─wrapper-component-1/
│ │ │ │ ├─wrapper component-1.[logic]
│ │ │ │ └─sub-component-1-2/
│ │ │ │ └─sub-component-1-2.[logic|markup|style]
│ │ │ └─shared/
│ │ │ └─shared-component-1.[logic|markup|style]
│ │ │
│ │ ├─route-component-2/
│ │ │ ├─route-component-2.[logic|markup|style]
│ │ │ └─README.md
│ │ │
│ │ │
│ │ └─shared/
│ │ └─shared-component-1.[logic|markup|style]
│ │
│ └─state/
│ ├─store-1/
│ │ └─store-1.[actions|reducer|state|effects]
│ │
│ └─store-2/
│ ├─store-2.[actions|reducer|state|effects]
│ └─README.md
│
├─assets/
│
└─node_modules/
Where most of the development work is done.
We divide it into two folders due to very different responsibilities that we want to keep decoupled.
These are:
-
App/
Routing and UI Components
-
State/
Everything related to state management, it's basically the BE of the FE
Here is where we keep UI components and respective complexity regarding what they should display, how they should look and how they are interacted with, not the state though.
Each Component is sort of it's own app. It should be easy to just copy it from it's location to another or to another project with the same FE framework even and get it to work quite seamlessly after making sure it has all it's dependencies.
+----------------+-------------------------+-------------+-----------+--------+
| Component Type | Communicates with State | Has a route | Has Logic | Has UI |
+----------------+-------------------------+-------------+-----------+--------+
| Route | yes | yes | yes | yes |
| Wrapper | yes | no | yes | no |
| Dumb | no | no | yes | yes |
+----------------+-------------------------+-------------+-----------+--------+
-
Route Components
Components that encapsulate routes. In the example we only have one level of route-components but it can by multi-level, each new route-component representing a new section in the url like:
/home/team
.Each
route-component
can have different architecture from otherroute-components
. And this is why it's so important that it handles it's own complexity entirely. This can seem confusing, shouldn't an app follow a unique architecture ? Ideally yes, but with the fast pace at which new packages and methodologies are developed it's unsustainable to refactor an entire app every time a developer wants to use something new. -
Wrapper Components
The single reason for wrapper components is to not handle communication with State Directly in Dumb components.
These components are pretty much always a wrapper for a specific dumb-component, not being needed to call the dumb component within when injecting them anywhere.
-
Dumb Components
Why components that only receive bindings or props ? Testability.
Not that I think think unit testing UI with more code is sustainable, but by using emerging tools like react story book we can visualize dumb components in multiple states at the same time by varying it's inputs.
-
Smart Components
No Need.
Route Components and Wrapper Components are kinda Smart components, with one exception: no local state!
It's advisable that routing logic is kept in each route-component-*
. In most online tutorials it's kept in the app-component
which seems intuitive since it makes it easy to detect what routes a project has from the app.routes
single file, but this goes against the idea of keeping the complexity at the respective level and makes it harder to move components around seamlessly and to have specific component logic.
When you notice that you are re-writing a component that is already in use somewhere else consider moving it to the /shared
folder. If a shared component is to be used in multiple route-components then it belongs in /app/shared
, if it's only used in one component (but still in multiple subcomponents of that component) then it belongs in ../component/shared
[UNFINISHED]
Here is where handle ALL state related complexity. Managing state at the /app should be avoided as much as possible.
Why?
-
Testing
State operations are a lot easier to test than components.
-
Separation of Concerns
-
Ease to move away from a FE framework
-
Map of all operations
-
Horizontal Architecture instead of Deep like components
The same way that if we want to try out different architectures and technologies in components we do it in route-components, at the State level we do it in Stores. That is, Stores don't have to behave exactly the same as each other and can use different state management technologies.
Since effects are not included in many Redux Libraries I'll quickly explain their use. But whatever Redux Library you are using it very likely to have an Effects Plugin.
Redux handles all state changes via pure and very simple reducers. But what about inpure stuff? Things like Async Operations, Server Requests, Redirects, anything else that doesn't belong in a reducer. Do we isolate those things in services or smart components ? No.
What we use are effects (whatever Redux Library you are using it very likely has an Effects Plugin).
Effects are very similar to Reducers with the exception that they don't alter the state, they do Async Operations and dispatch actions.
Let's look at an example
[LOGIN] USER_LOGIN (when user submits the login form)
In a reducer >
updates state with credentials
In an effect > makes login API request,
[LOGIN] USER_LOGIN_SUCCESS (if success)
In a reducer > updates state to set user as authenticated
[LOGIN] USER_LOGIN_FAILURE (if fail)
In a reducer > updates state with login failure message
As we can see above the Effects handles all the Async Logic, dispatches the respective success or unsuccess actions that trigger state changes in the reducer all while our components are listening to this state changes, updating themselves in relation to the state but totally oblivious to what's affecting it, making them a lot simpler.
There are currently many architectures for Redux style state management, this is the one I have found most appropriate for fast development, complexity encapsulation and scaling.
Root files are mostly configurations for our project.
Some common ones are:
-
Linters
Rules on how we should write our code, these rules are enforced by warnings from our Code Editors and while running certain commands
e.g. eslint, tslint, csslint, sass-lint, stylelint, ...
-
Task Runners
Command Line Commands (and the logic behind them) that we can use on our project
e.g. package.json (npm scripts inside), gulp, grunt, bash scripts, ..
-
Configurations and Meta Information
package.json, .gitignore, .angular-cli.json, karma.conf.js, .editorconfig
add index.html file to src