Enterwell React starter
Enterwell's template for web apps based on the React and Next.js.
This document represents the official React starter documentation. React starter was created due to the desire to unify all React projects that we will develop in the future. This document will therefore explain and list out the following conclusions that the Enterwell elders agreed on after several multi-hour meetings:
If any doubts remain after reading this document, feel free to contact us via GH Issues.
This project uses pnpm as its package manager so in order to get quickly up and running you will need to have it installed on your machine.
If you don't already have it, you can easily install it by using the following command (assuming you have Node.js installed)
npm install --global pnpm
Now you can setup the application without any hassle using the following command
pnpm create next-app -e https://github.com/Enterwell/react-starter
Create a new local configuration .env.local
by using the provided example file using the command
cp .env.local.example .env.local
And success, you are ready to rumble!
Once in the project directory, you can start the development
version of the application using the command
pnpm dev
For documentation on running the application in other modes, see 'Launching the application' section.
You can remove features you don't plan to use by calling pnpm feature:remove <FEATURE>
. Feature that can be removed are as following:
storycap
storybook
(also removed storycap
)playwright
jest
React is one of many JavaScript libraries and frameworks that aim to make building user interfaces easier. React is Facebook's project, which ensures a certain certainty that this is not just another buzzword technology that will be forgotten in a month. Why was React chosen for application development, and not, say, Vue or Angular, I do not know, but since it turned out to be quite easy and fast to develop applications with it, there was no need to change.
Next.js is one of React frameworks that we relatively recently decided to use together with React. Its use makes it easier to configure projects (without having to mess around with webpack), provides support for pre-rendering pages, and more.
The React starter's root contains all the configuration files of the tools used during the development and build of the application, as well as folders with different aspects of the application.
.eslintrc
- used for configuring ESLint.eslintignore
- used for defining files that will be ignored by ESLint.gitignore
- used for defining files which changes Git will not trackpackage.json
- used for defining packages used in the application (so-called dependencies
and devDependencies
)playwright.config.js
- used for configuring Playwrightplaywright-ct.config.js
- used for configuring Playwright's component testsjest.config.js
- used for configuring Jestpnpm-lock.yaml
- used by pnpm to know exactly which versions of the packages need to be installednext.config.js
- used for defining non-default Next.js configurationREADME.md
- used for project description - how to get it started, some basic things about the packages used or some other tips for people who will work on the project in the futureCHANGELOG.md
- used for keeping application change logs (adding new features, fixing bugs, etc.).storybook
- a folder used for Storybook configuration which contains various configuration files.stories-approved
- a place where images of all of the defined Storybook
stories are stored.playwright-approved
- a place where all Playwright screenshots are storedapp
- a folder which Next.js uses for its file-system based app routerapp-models
- a place where all the app-models that exist within the application are storedcomponent-models
- a place where all the component-models that exist within the application are storedcomponents
- a place where all components that are not related to only one view
are stored (so-called shared components)config
- a place where the various configuration files, used by the application itself, are stored (e.g. internationalization configuration, MUI themes, Next.js fonts or something else)playwright
- a place where Playwright related files are storedhelpers
- a place where all the helpers that exist within the application are storedhooks
- a place where all custom hooks that exist within the application are storedmappers
- a place where all the mappers that exist within the application are storedmodels
- a place where all the models that exist within the application are storedpublic
- a place where all the static resources of the application are stored (e.g. images, SVGs and files that can be downloaded through the application)repositories
- a place where all the repositories that exist within the application are storedservices
- a place where all the services that exist within the application are storedstyles
- a place where all the global styles that exist within the application are storedtests
- a place where all of the tests that exist within the application are storedview-models
- a place where all the view-models that exist within the application are storedviews
- a place where all the views and only related components are storedMore details on what each of these entities is can be read in the section on the architecture of the React starter application. Additional note: it is possible to find, in some folders, a file named TODO_delete_this_later.txt
which sole purpose is to make the folder not empty so that Git can track it.
Let's start now in medias res - the image just below this paragraph shows the architecture of Enterwell React applications. Of course, this is not the only, and at least the only correct architecture of the React application, but it is an architecture around which the elders of Enterwell almost unanimously agreed. Different segments of the architecture will be explained below.
flowchart LR
Component --> AppModel
View --> ViewModel
ViewModel --> Repository
Repository <--> Mapper
Mapper --> Model
AppModel --> Repository
Repository --> external
subgraph external[External resources]
direction LR
ExtApi[API]
ExtGraph[GraphQL]
ExtOther[Other resources...]
end
Missing from diagram above is AppModel
since it's not used that often. It fits into above diagram as follows.
flowchart LR
S1[...] --> ViewModel
S2[...] --> Component
ViewModel --> AppModel
Component --> AppModel
AppModel --> Repository
Repository --> S3[...]
At the heart of any React application are its components. Components are the building blocks of an application and define the user interface that the user will ultimately see. In Enterwell's React architecture, components can correspond to one of the following 3 groups: app
, views
, and components
.
App
React applications developed at Enterwell in previous years used react-router
and similar packages for routing. By switching to Next.js, the need for using these packages has disappeared and the routes of applications are defined by a hierarchy of files and folders within app
folder (e.g. app/(routes)/page.jsx
file matches the route /
, app/(routes)/pokemons/(routes)/page.jsx
file matches the route /pokemons
etc.).
Since this way of routing is typical of Next.js, and due to the desire to make applications a little less coupled with it, app
components serve only as an encapsulation around the views
components.
It is important to note that within the app
folder there are also files that do not correspond directly to the application routes. This refers to layout.jsx
, error.jsx
and not-found.jsx
files that have a special role defined by Next.js app router.
These are only a few of many special file conventions that create the UI in a Next.js app router application.
Views
views
components represent everything that the user sees on an application route, and they can then use one or more "ordinary" components within themselves. If the view
component becomes too complex, it is recommended to break it down into more "ordinary" components. If the "ordinary" components obtained in such a way are used only in that view
and nowhere else, they need to be placed in a separate folder within the folder of that view
. Components used in multiple places in the application need to be placed in a separate folder within the components
folder.
Components
components
components represent all those components that are used in several places in the application. A component should be placed in this folder if it is used in at least two places in the application or if it is general enough to be used in multiple places. Once the development of the application has started, all components will be used in only one place, but some of them can be assumed in advance that they can be used in more places. Examples of such components are various Input
components.
Each React component has support for data persistence in the form of its state
. If the application has slightly larger components with a larger amount of data, the state
of these components can easily become obscure, and the components themselves become too polluted with the application's logic. To address this problem, over time, libraries have emerged that simulate state
behavior (in the sense that they activate component re-render when data on which it depends changes) but also allow data on which the component depends to be stored outside of it. One of these libraries is mobx
which is used in Enterwell React applications.
In general, both data storage methods are combined within Enterwell React applications. When it comes to forms or some components with a small amount of data that need to be stored, then state
is used for storage. When it comes to larger components with more complex logic, then data and logic are separated from the components into separate units. The following is a hierarchy of units for data persistence in the application.
App-models persist data that is common to the entire application and that should be preserved throughout the use of the application, regardless of the route on which the user is located.
App-models can be accessed directly or through a view-model that needs to keep a reference to it. You should almost always use the latter way of accessing app-models. The former method should only be used for some top components that do not belong directly to any view
(e.g. layout component which is the same for most views
and is defined within the common _app.jsx
component which does not have its own view-model).
In addition to data, app-models also contain logic for retrieving and modifying them.
In view-models, the data that is characteristic to a view
persists. Depending on the needs, view-models can be destroyed along with the view
or can exist throughout the life of the application. When the application contains a view
with a list of some data (e.g. a list of all Pokemon), then it is appropriate to use view-models that are created only once and that exist throughout the life of the application. For views
that show the details of the list elements (e.g. details of a Pokemon), it is appropriate to use view-models that last as long as the view
to which they belong. In the latter case, short-term view-models are suitable because the same view
is used to display several different routes (e.g. /pokemons/1
, /pokemons/45
etc.) and with this, the case of incorrect data displaying during the initial component rendering is avoided.
The above cases are just examples and the lifespan of a view-model depends solely on the needs of an application. Also, just like app-models, view-models contain logic in addition to data to retrieve and modify it.
Data that is inherent to a component
persists in component-models. Component-models should not be made for all components, but only for those with more complex logic (e.g. when the same modal is used to create something on multiple views
in the application, it is more appropriate to separate logic into a component-model than to repeat it in each view model and then forward it to the component).
It has already been mentioned that part of the application logic is located in app-models, view-models and component-models. The logic distributed across these units should be closely related only to the data they contain. Looking at the previous picture, it is evident that there is another layer of logic which segments will be discussed in this section.
Models are classes that represent entities used in an application. Data retrieved from the server (or other data source) needs to be mapped to appropriate models.
Mappers are sets of functions that provide a data mapping service. The most common use case is to use them when mapping data from the server to the models used in the application. When mapping, it is possible to make appropriate transformations over individual data (e.g. data formatting, data localization, etc.).
Repositories are sets of functions that serve as application boundaries and through which the application retrieves data. How the data will be retrieved depends solely on the repository or the application itself. The most common way to retrieve data is from an API, but data can also be retrieved from local storage
, for example.
Repository methods use individual mappers to map the data they retrieve.
Services are sets of functions that provide an application-specific role, such as displaying notifications, communicating with local storage
, communicating with an API or something else.
Helpers are sets of functions very similar in purpose to services, but still a little less specific and they usually provide some "stupid" service that is repeated in several places in the application.
Hooks are similar in purpose to helpers, but they are primarily focused for being used in React components. They let us extract component logic into reusable functions. Hooks are JavaScript functions whose name starts with use
and that may call other Hooks.
flowchart TB
App --> IndexView
App --> PokemonsView
App --> PokemonDetailsView
App --> UserInformations
UserInformations --> UserAppModel
PokemonsView --> PokemonsViewModel
PokemonsViewModel --> PokemonsRepository
PokemonDetailsView --> PokemonDetailsViewModel
PokemonDetailsViewModel --> PokemonsRepository
PokemonDetailsViewModel --> UserAppModel
UserAppModel --> UsersRepository
PokemonsRepository --> external
subgraph subPokemonsRepository[Pokemons Repository]
direction BT
PokemonsRepository <--> PokemonsMapper
PokemonsMapper --> PokemonDetails
PokemonsMapper --> PokemonSimplified
end
UsersRepository --> external
subgraph subUsersRepository[Users Repository]
direction BT
UsersRepository <--> UsersMapper
UsersMapper --> User
end
subgraph external[External resources]
direction LR
ExtApi[API]
ExtGraph[GraphQL]
ExtLocalStorage[Local storage]
ExtOther[Other resources...]
end
For all this not to be just a dead letter on the screen, a smaller application that implements the previously described architecture was created as part of the React starter. The application uses PokéAPI and, as you can already guess, it is used to view Pokemon.
The application consists of 3 "smart" and 2 "stupid" pages. Stupid pages are those to which the user will not otherwise voluntarily come to watch the content, but will be redirected there in certain situations. Those 2 "stupid" pages are app/error.jsx
and app/not-found.jsx
. app/error.jsx
is displayed to the user when an application error occurs, and app/not-found.jsx
when the user enters a route that is not defined. The "smart" pages are app/(routes)/page.jsx
(corresponding to route /
), app/(routes)/pokemons/(routes)/page.jsx
(corresponding to route /pokemons
) and app/(routes)/pokemons/(routes)/[pokemonId]/page.jsx
(corresponding to route /pokemons/{pokemon-id}
). The /
route page in this application only displays the message that nothing can be seen there and directs the user to the Pokemon page. /pokemons
route displays a list of Pokemon with pagination. Clicking on an individual Pokemon from the list opens the /pokemons/{pokemon-id}
route page showing its details. The /pokemons
and pokemons/{pokemon-id}
routes in the upper right corner display a component where the user can enter their name.
Since the /
route does not store any data, no logic is tied to it, so it will not be mentioned below.
The /pokemons
route displays a list of Pokemon retrieved from PokéAPI. The component corresponding to that page keeps a copy of the PokemonsViewModel
inside of itself. Although this view model is retrieved each time the user arrives on that route, there is only one instance of that view model while using the application (unless the page refreshes or the page is closed and reopened). Using the PokemonsViewModel
methods, the component calls to retrieve the data and then displays it. The PokemonsViewModel
uses the PokemonsRepository
which communicates with an API, to retrieve Pokemon. After retrieving the data from an API, PokemonsRepository
forwards the data to the PokemonsMapper
which maps it to a collection of PokemonSimplified
models and returns it to the repository. The repository then gives the mapped data to the PokemonsViewModel
.
The /pokemons/{pokemon-id}
route shows Pokemon details retrieved from PokéAPI. The component corresponding to that page keeps a copy of the PokemonDetailsViewModel
. Unlike PokemonsViewModel
, there is not just one instance of PokemonDetailsViewModel
, but a new one is created each time (each time a user comes to that page). Retrieving Pokemon details works the same as retrieving a list of them - the same repository and mapper are used, and only the data is mapped to another model. Another difference of this page in relation to pokemons/index.jsx
is that this page needs information about the name of the current user which is stored at the application level in UserAppModel
. UserAppModel
instance is therefore accessible to the page through its view-model. UserAppModel
retrieves user data using UserAppRepository
which communicates with local storage
.
The components used within this demo application are not necessarily compatible with the components that should be used on "real" applications. This primarily refers to the LoadingContainer
component around which the spears are broken and which according to some should behave differently.
To make it easier to develop components (whether component
or views
) and isolate them from the rest of the application and its business logic during their development, we use Storybook. Storybook is an open-source tool that allows us to build UI components and pages in isolation. It does not need to run the entire, possibly complex, dev stack, force test data, and navigate the entire application to develop a single component. We use Storybook for shared components located in the components
folder and for pages that are harder to trigger eg. error pages. In our case that would be NotFoundView
and InternalServerErrorView
.
In order to add new component to Storybook
it is necessary to define a story file within the same folder as the component file, which can be identified by the .stories.tsx
extension. You can read up on how to write a story here.
Starting the Storybook
UI interface is done with the command
pnpm storybook
This command will start Storybook
locally and output the address at which the process is running. Depending on your system configuration, the address will automatically be opened in a new browser tab.
If you don't plan to use Storybook you can remove it using pnpm feature:remove storybook
. Note that storybook is required for storycap functionality and will be removed if storybook is removed.
Each component (whether component
or view
) should have its own styles. Styles are placed in the same folder as the component file and the style file can be identifier by the .module.scss
extension. The exception to this rule are global styles that apply to the entire application and are located in the styles
folder. Colors that are used in the application are defined the same way as global styles. This way, they can be used in several places in the application without having to rewrite them over and over again (this is especially convenient for the main colors of the theme that runs throughout the application). It is important to note that not all colors should be extracted into global styles, especially if they will be used only in one place.
Naming is something that always provokes controversy because most of us have some style of our own that we prefer. Even while negotiating this React starter there were such problems, and everyone was wanting to have it their own way. It was difficult and the creators of React were not very helpful as there is no official convention. Observing other React projects, we came to the decisions described below.
kebab-case
is used (all words are written in lower case and are separated by a hyphen)PascalCase
is used (all words are capitalized and are not separated from each other)app
and public
folders which are also written with kebab-case
app
and styles
folders are written with kebab-case
exported
are written with PascalCase
exported
are written in camelCase
(the first word is written in lower case, the rest in uppercase and they are not separated from each other)PascalCase
with extension .module.scss
public
folder are not subject to any rulesBy writing tests we achieve automated checks that everything in the application is working properly. Automated tests are useful because you don't have to manually test all the functionalities every time something changes in the application. All of our tests are written inside the tests
folder in the project root.
We have selected both Jest and Playwright as the most suitable libraries for testing the application. When we talk about application testing, we can divide all tests into three different logical levels.
If you don't plan to use testing that is configured as a part of this starter - use pnpm feature:remove playwright jest
to remove everything related to these tests.
Unit testing is different from other testing methods because it consists of testing isolated parts of the source code, testing the code and logic. We use them in the application to test services
and helpers
or any other JavaScript code when there is some advanced logic. We will never use unit tests, or any other testing method, for testing third-party code directly, because if we depend on a certain package, we must be able to assume that it will work properly.
Unit tests are written inside the tests/unit
folder. There is only 1 unit test currently written in the application. It can be recognized by the spec.js
extension. LocalStorageService.spec.js
is testing the corresponding LocalStorageService.js
service.
Unit tests can be run directly from the command line using the command
pnpm test:unit
Component testing is conceptually the same as unit testing, except that instead of functions, we test components. We want to write these tests because the number of components can very easily reach a large number. Now, after each change in the source code, it becomes almost impossible to check all of their states to see if they still behave as expected.
Component tests are written inside the tests/component
folder. There are already tests written for each component in the application from the components
folder. They can be recognized by the spec.jsx
extension.
Component tests can be run directly from the command line using the command
pnpm test:component
or a UI can be opened through which they can be manually run. The UI is opened by using the command
pnpm test:component-open
E2E or end-to-end tests are used to verify that the application is working as a whole. They confirm big features and even entire pages. Most often, they "survive" refactoring because, despite refactoring, application still needs to work as expected. They represent how users use the application and give us the most confidence that the application is working properly.
End-to-end tests are written inside the tests/integration
folder. There are already tests written for each page of the application from the views
folder. They can be recognized by the spec.js
extension.
End-to-end tests can be run directly from the command line using the command
pnpm test:e2e
or a UI can be opened through which they can be manually run. The UI is opened by using the command
pnpm test:e2e-open
Various different commands were shown that can run each testing method separately from each other. This can be useful if we want to focus on one type of tests without running others. But we can also run all of the tests at once by using the command
pnpm test
In end-to-end tests, we can use Playwright
's handy screenshot command. Using this command we can generate a screenshot of the application under test at any desired moment. This can make it easier for us to review PRs and all future changes.
The command accepts many parameters for image format, clip area, quality, etc.
We have defined our own screenshot
function that wrapps the Playwright's
in the playwright/helpers/PlaywrightHelpers.js
module. The reasoning behind the wrapper was to normalize the directory that will contain the screenshots so we can use it later in our CI (continuous integration) pipeline. Our wrapper stores the screenshots in the root .playwright-pending
directory following the hierarchy:
.playwright-pending
├── <test_file_name>
│ ├── <screenshot_name>-<browser_name>.png
│ └── ...
...
An example of calling the screenshot command:
...
test('shows the Pokemons data', async ({ page, browserName }) => {
await screenshot(page, browserName, __filename, 'pokemonList');
});
...
We have also defined the following script within project.json
pnpm e2e-check
which runs our custom logic contained in the ScreenshotsCompare
helper.
This script is run automatically on all PRs to the main
and feature/**
branches, as well as on pushes to the main
branch by the .github/workflows/BuildAndTest.yml
GitHub Workflow. The workflow creates a commit with the screenshots to the PR branch.
This gives us an easy way to get a visual comparison of the tests that have changed when reviewing the PRs.
By using Playwright
as our end-to-end testing tool we have been given an option to extend existing or create new tests entirely by clicking and recording interactions against the running application.
By looking at package.json
you can get an idea of some of the packages used. The React starter includes only the basic packages that we think will always be used in the application, but there is also a whole set of other packages that are used as needed.
One of the questions that come up when adding packages is whether they should be added as dependencies
or as devDependencies
. The limit is blurry, but let's say that only the packages without which the application can not work on production should be added as dependencies
, while devDependencies
should only include packages used during development. You don't need to worry so much about it, because the applications we develop will work properly regardless of that.
The following are packages that have been added to the project by default and will most likely be used in the application. If it turns out that there is no need for some of them, they can be removed freely. Note: packages that are tools to help the development and the build of the application (babel
, eslint
, next
, etc.) are not described.
react
/ react-dom
- library which role has already been described and which name is in the name of the starter, which implies that it is impossible not to usemobx
/ mobx-react-lite
- state management library that allows you to separate application logic from rendering components (allows data changes to cause components to render - it has a similar effect as state components but it is not necessarily related to it)playwright
- library that allows you to write tests for the application@mui/material
/ @mui/icons-material
- a collection of React components and icons that allows us not to reinvent the wheel by writing our own buttons, inputs and other componentsaxios
- HTTP client that allows easy communication between the application and the server or an APInoty
- library used to display notifications within the applicationclsx
- package that simplifies conditional class assignment to HTML elements / React componentsSince the goal of this starter is not to bloat or overload it with all the possible packages that could be used, many of them will need to be added as needed. The following is a list of packages that are recommended for use if the described functionality of the application provided by that package is needed.
If none of the following packages meet your wishes and demands, you need to add a new one of your choice. npm is a place where you can find packages for everything.
@enterwell/react-form-validation
- our own, "homemade" package for working with React forms that primarily allows for their easy validation@enterwell/enum-helper
- another "homemade" package that makes it easier to work with enums@mui/lab
- an additional collection of MUI components that have not yet become an integral part of the core collection@material-ui/pickers
- a collection of time and date picker components also developed by the MUI (formerly known as Material-UI) teamsentry
- application error tracking package (so-called error monitoring)i18next
/ react-i18next
- framework for internalization of applications with minimal overheadmoment
- library that makes it easier to work with dates and timesFor a more organized development of components and pages, we use Storybook
as we have already mentioned earlier. One of its add-ons we use is called Storycap.
Storycap
crawls Storybook
and generates images of all defined stories. With the help of these generated images, we can make it easier for us to review PRs and all new future changes.
If you don't plan to use storycap you can remove it using pnpm feature:remove storycap
.
Script has been defined within project.json
that is used for this purpose.
pnpm stories-check
which runs Storycap
and places generated images in the .stories-pending
folder in the project root. The script will then run our custom logic contained in the StoriesCompare.js
helper.
At the end of the execution, images of all of the stories that have changed in this development iteration have now been modified and overwritten in the .stories-approved
folder (either because we modified the components or because we added, modified, or deleted some of the stories).
This script is run automatically on all PRs to the main
branch by the .github/workflows/ScreenshotsCheck.yml
GitHub Action. The workflow creates a commit with the modified images to the PR branch.
This gives us an easy way to get a visual comparison of the stories that have changed when reviewing the PRs.
There are several commands with which you can launch the application and it all depends on whether you want it to run in development
or production
mode.
Starting the application in development
mode is done with the command
pnpm dev
When application development is complete, the application needs to be built
. Building
the application is done using the command
pnpm build
When you have successfully built
your application, you can start the production
version using the command
pnpm start
Whether running in development
or production
mode, application is available at http://localhost:3000.
Exporting your application to static HTML, which can then be run standalone without the need of a Node.js
server is done using the command
pnpm export
This command generates an out
directory in the project root. Only use this command if you don't need any of the features requiring a Node.js
server.
Before deploying the application, make sure that all the tasks from the list below have been completed.
next.config.js
package.json
app/layout.jsx
and several pages where the specific page titles are givenPokemonsMapper.js
, PokemonsRepository.js
...)TODO_delete_this_later
files and empty foldersNEXT_PUBLIC_API_URL
environmental variable (https://localhost:5001/v1/
is the default)