Tutorial
Let's consider the application of Feature-Sliced Design on the example of TodoApp
- At first, we will prepare application basely (bootstrap, routing, styles)
- Then we will consider - how the concepts of the methodology help flexibly and effectively design business logic without unnecessary costs
There is codesandbox-insert with the final solution, which can help to clarify the implementation details at the end of the article
Stack: React, Effector, TypeScript, Sass, AntDesign
The tutorial is designed to reveal the practical idea of the methodology itself. Therefore, the practices described here are largely suitable for other technological stacks of frontend projects
1. Preparationโ
1.1 Initializing the projectโ
At the moment, there are many ways to generate and run a project template
We will not focus too much on this step, but for quick initialization, you can use CRA (for React):
$ npx create-react-app todo-app --template typescript
1.2 Preparing the structureโ
We received the following blank for the project
โโโ src/
โโโ App.css
โโโ App.test.tsx
โโโ App.tsx
โโโ index.css
โโโ index.ts
โโโ logo.svg
โโโ react-app-env.d.ts
โโโ reportWebVitals.ts
โโโ setupTests.ts
โโโ index.tsx/
How it usually happensโ
And usually most projects at this stage turn into something like this:
โโโ src/
โโโ api/
โโโ components/
โโโ containers/
โโโ helpers/
โโโ pages/
โโโ routes/
โโโ store/
โโโ App.tsx
โโโ index.tsx/
They can become such immediately, or after a long development
At the same time, if we look inside we will most likely find:
- Highly coupled directories by nesting
- Strongly connected components with each other
- A huge number of dissimilar components / containers in their respective folders, linked thoughtlessly
How can it be done otherwiseโ
Anyone who has been developing frontend projects for at least a long time understands the advantages and disadvantages of this approach.
However, most frontend projects are still something like this, since there is no proven flexible and extensible alternative
Multiply this by the free adaptations of the structure for each project, without a ban from the framework-and we get "projects as unique as snowflakes"
The purpose of this tutorial is to show a different view of the usual practices in designing
Adapting the structure to the desired viewโ
โโโ src/
โโโ app/ # Initializing application logic
| โโโ index.tsx # Entrypoint for connecting the application (formerly App. tsx)
| โโโ index.css # Global application styles
โโโ pages/ #
โโโ widgets/ #
โโโ features/ #
โโโ entities/ #
โโโ shared/ #
โโโ index.tsx # Connecting and rendering the application
At first glance the structure may seem strange, but over time you will notice that you use familiar abstractions, but in a consistent and ordered form.
Also, we enable support for absolute imports for convenience
{
"compilerOptions": {
"baseUrl": "./src",
// Or aliases, if it's more convenient
Here's how it will help us in the future
- import App from "../app"
- import Button from "../../shared/ui/button";
+ import App from "app"
+ import Button from "shared/ui/button";
Layers: appโ
As you can see , we have moved all the basic logic to the app/
directory
It is there, according to the methodology, that all the preparatory logic should be placed:
- connecting global styles (
/app/styles/**
+/app/index.css
) - providers and HOCs with initializing logic (
/app/providers/**
)
For now, we will transfer all the existing logic there, and leave the other directories empty, as in the diagram above.
import "./index.css";
const App = () => {...}
1.3 Enabling global stylesโ
Install dependenciesโ
In the tutorial, we install sass, but you can also take any other preprocessor that supports imports
$ npm i sass
Creating files for stylesโ
For css variablesโ
:root {
--color-dark: #242424;
--color-primary: #108ee9;
...
}
To normalize stylesโ
html {
scroll-behavior: smooth;
}
...
Connecting all stylesโ
@import "./normalize.scss";
@import "./vars.scss";
...
@import "./styles/index.scss";
...
import "./index.scss"
const App = () => {...}
1.4 Adding routingโ
Install dependenciesโ
$ npm i react-router react-router-dom compose-function
$ npm i -D @types/react-router @types/react-router-dom @types/compose-function
Add HOC to initialize the routerโ
import { Suspense } from "react";
import { BrowserRouter } from "react-router-dom";
export const withRouter = (component: () => React.ReactNode) => () => (
<BrowserRouter>
<Suspense fallback="Loading...">
{component()}
</Suspense>
</BrowserRouter>
);
import compose from "compose-function";
import { withRouter } from "./with-router";
export const withProviders = compose(withRouter);
import { withProviders } from "./providers";
...
const App = () => {...}
export default withProviders(App);
Let's add real pagesโ
This is just one of the routing implementations
- You can declare it declaratively or through the list of routes (+ react-router-config)
- You can declare it at the pages or app level
The methodology does not yet regulate the implementation of this logic in any way
Temporary page, only for checking the routingโ
You can delete it later
const TestPage = () => {
return <div>Test Page</div>;
};
export default TestPage;
Let's form the routesโ
// Or use @loadable/component, as part of the tutorial - uncritically
import { lazy } from "react";
import { Route, Routes, Navigate } from "react-router-dom";
const TestPage = lazy(() => import("./test"));
export const Routing = () => {
return (
<Routes>
<Route path="/" element={<TestPage/>} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
);
};
Connecting the routing to the applicationโ
import { Routing } from "pages";
const App = () => (
// Potentially you can insert here
// A single header for the entire application
// Or do it on separate pages
<Routing />
)
...
Layers: app, pagesโ
Here we used several layers at once:
app
- to initialize the router (HOC: withRouter)pages
- for storing page modules
1.5 Let's connect UIKitโ
To simplify the tutorial, we will use the ready-made UIKit from AntDesign
$ npm i antd @ant-design/icons
But you can use any other UIKit or create your own by placing the components in shared/ui
- this is where it is recommended to place UIKit of application:
import { Checkbox } from "antd"; // ~ "shared/ui/checkbox"
import { Card } from "antd"; // ~ "shared/ui/card"
2. Implementing business logicโ
We will try to focus not on the implementation of each module, but on their sequential composition
2.1 Let's analyze the functionalityโ
Before starting the code, we need to decide - what value we want to convey to the end user
To do this, we decompose our functionality by responsibility scopes (layers)
Pagesโ
We will outline the basic necessary pages, and user expectations from them:
-
TasksListPage
- the "Task List" page- View the task list
- Go to the page of a specific task
- Mark a specific task completed/unfulfilled
- Set filtering by completed / unfulfilled tasks
-
TaskDetailsPage
- page "Task card"- View information about the task
- Mark a specific task as completed/unfulfilled
- Go back to the task list
Each of the described features is a part of the functionality
Usual approachโ
And there is a great temptation
- or implement all the logic in the directory of each specific page.
- or put all" possibly reused "modules in the shared folder
src/components
or similar
But if such a solution would be suitable for a small and short-lived project, then in real corporate development, it can put an end to the further development of the project, turning it into "another dense legacy"
This is due to the usual conditions of the project development:
- requirements change quite often
- there are new circumstances
- the technical debt is accumulating every day and it is becoming more difficult to add new features
- it is necessary to scale both the project itself and its team
Alternative approachโ
Even with the basic partitioning, we see that:
- there are common entities between the pages and their display (Task)
- there are common features between the pages (Mark the task completed / unfulfilled)
Accordingly, it seems logical to continue to decompose the task, but already based on the above-mentioned features for the user.
Featuresโ
Parts of functionality that bring value to the user
<ToggleTask />
- (component) Mark a task as completed / unfulfilled<TasksFilters/>
- (component) Set filtering for the task list