This template is designed to ease the bootstrapping process of a new frontend project.
It includes the following:
- Basic Next.js setup with TypeScript support
- Eslint, Prettier, Stylelint and Conventional Commits configurations. Note that we use AirBnb style guides for our TypeScript code.
- Basic file structure. See Practices section for details.
- This readme file. Use this as a reference on how we approach making frontend experiences at Coreblocks.
Step 1. Clone this repo.
git clone git@github.com:coreblocks/frontend-template.git
Step 2. Install NVM.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
Step 3. Install Node via NVM
source scripts/useNodeVersion.sh
Step 4. Install dependencies
npm ci
Caution: Always prefer npm ci
over npm i
when not installing
any new packages. This helps preserve dependency version freeze, since running
npm i
can update indirect dependencies.
Step 5. Run the app.
npm run dev
All contributions should be done in feature branches. No code should ever be
pushed directly to master
.
A feature branch represents all the work associated with a single Jira ticket.
As such, naming should follow the specific ticket you're doing work on.
EX.: CYBERCREW-123.
Feature branches must not ever be directly merged to master
. All feature
branches must go through the pull request & code review process before merging.
Commit messages should be exclusively in English. Messages should be prefixed with these common semantics, followed by a colon:
feat
— changes represent a feature, something new.fix
— changes represent a bug fix or an infrastructure fixchore
— changes consist of tidying up, mostly (ex.: dependency bump, resource file sync, etc.)refactor
— changes represent refactoring of existing code
Examples:
feat: user profile page implementation
feat: new Drawer component
fix: serialization for transport layer
fix: proper handling of empty string values
chore: bump react version
chore: move global styles to a separate directory
refactor: new plugin init flow
Focus on describing the changes as concisely as possible.
Commit frequency doesn't matter. However, you should note that pushing one big commit at the end of a feature development would generally make your code much harder to review.
Comments should be exclusively in English. You shouldn't overdo comments, however. Good code doesn't need explanation for every single statement. You should probably leave comments only for really non-trivial parts of your code, or when you're doing some dirty hacks with good reasoning.
JSDoc is greatly encouraged, however, its usage is not imperative. Common utilities and components should probably have it, though.
Your should follow these rules when organizing your code:
-
All file names should be in
camelCase
for utilities/aux and inPascalCase
for components/classes. Directory names should be inPascalCase
for components and inkebab-case
for everything else. -
Everything code-related should end up in
src/
. Keep in mind the following subfolder rules. -
All components should be placed in
components/
folder. Every component should have its own directory there. The actual.tsx
file should be also named the same as a directory it resides in. Exception: auxilliary components that complement the main component, e.g. additional views or something like that — this stuff can be just placed in the same directory as the main component.The corresponding CSS module should follow the same naming convention.
Other than that, the component folder structure is more or less flexible.
Example:
components/ --| AppHeader/ ----| AppHeader.tsx ----| AppHeader.module.css --| ModalWrapper/ ----| ModalWrapper.tsx ----| ModalWrapper.module.css ----| useEventHandlers.ts --| ComplexCompositeComponent/ ----| generic-views/ ------| definitions.ts ----| SomeAuxComponentUsedOnlyHere ------| SomeAuxComponentUsedOnlyHere.tsx ----| ComplexCompositeComponent.tsx ----| ComplexCompositeComponent.module.css ----| useSomeHook.ts ----| LocalMobxStore.ts ----| types.ts
-
Common utilities (non-component ones) should be placed in
utils/
directory. Utility hooks should be placed inutils/hooks/
. Examples of common utilities: date formatting utilities,sleep
async helper, something that performs a non-standard sorting of data, etc.You can also make your own subdirectories in
utils/
as you see fit. -
Global styles/common style utilities should be placed in
styles/
. -
Global type definitions (
.d.ts
) should be placed intypings/
. -
CI scripts and everything CI-related should be placed in
ci/
. -
Miscellaneous scripts (e.g. node version installer or an API sync) should be placed in
scripts/
. -
Unit tests should be placed near the piece of code they're testing. Please note that you should follow the naming of a file these tests are based on.
Ex.:
someConverterFunction.ts someConverterFunction.spec.ts
-
Integration tests should be placed in
tests/integration/
.Ex.:
tests/ --| integration/ ----| userLogin.spec.ts ----| walletConnect.spec.ts
-
Mocks and fixtures/stubs should be placed in
tests/mocks/
andtests/fixtures
respectively. -
Static assets (those that end up in the bundle or those requiring a specific loader) should be placed in
assets/
.- Likewise, I18N data should be placed in
assets/i18n/
.
- Likewise, I18N data should be placed in
-
Everything related to transport as well as global state/stores/data objects should be placed in
domain/
.
Tests are not imperative. However, it makes a lot of sense to have them for common utilities and data conversion functions.
Follow these rules when writing your stylesheets:
-
We use plain CSS for all stylesheets.
-
We use CSS modules for all non-global styles.
-
All custom identifiers in CSS modules should always follow
camelCase
naming. -
Avoid BEM and/or other strict naming conventions. Be concise and descriptive with your class names.
/* bad */ .text-input__control {} .text-input__control_size-s {} .TextInput-Control {} .some-class-name-in-kebab-case {} /* good */ .control {} .control.sizeS {} .panelWrapper {} .additionalControls {}
-
CSS Custom Properties (aka CSS variables) are encouraged. Use
kebab-case
as a naming convention. Avoid all camelization./* bad */ --borderRadius: 4px; --blurEffect: blur(20px); /* extra bad */ --abstract-borderRadius: 4px; /* good */ --border-radius: 4px; --color-surface: white; --surface-elevated-effect-box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
-
Use of class components is strongly discouraged.
-
Conversely, use of functional components is strongly encouraged.
-
Don't use default exports (exception: Next route components). Always use named exports.
-
Provide prop types for components as a separate type alias.
-
Annotate function components with
React.FC<PropType>
.Ex.:
type ButtonProps = { type: 'primary' | 'outline'; size: 's' | 'm' | 'l'; onClick: () => void; mix?: string; } export const Button: React.FC<ButtonProps>;
This also works with Mobx
observable
HoC.type UserViewProps = { name: string; } export const UserView: React.FC<UserViewProps> = observable(() => null);
-
Use of
useCallback
is discouraged. Consider usinguseStableCallback
instead. -
If your component has complex state, use either a single
useState
call with an object state type or Mobx. -
Never use list indexes as a
key
. Exception: static list data which is known at compile-time and is never changed. -
Consider memoizing when passing an object state to the context.
-
Never use
any
. Consider usingunknown
or a type cast instead. -
Use of interfaces is discouraged. Use type aliases instead.
-
When making a string-indexed type (e.g. a map of ids to some data), consider specifying value type as
T | undefined
./* bad */ type StateMap = Record<string, StateType>; const states: StateMap = {}; // will compile fine, even though no check for key existence is done states[someKey].someStateField; /* better */ type StateMap = Record<string, StateType | undefined>; const states: StateMap = {}; // now we need optional chaining to access the field states[someKey]?.someStateField; /* best */ const states = new Map<string, StateType>(); // Map access can returns a nullable of StateType states.get(someKey)?.someStateField
-
Always use
// @ts-expect-error
directive instead of// @ts-ignore
. -
Only use
// @ts-expect-error
for really non-trivial cases. -
Don't prefix types with
I
./* not good */ type IDisposable = { dispose: () => void; } /* good */ type Disposable = { dispose: () => void; }
-
Use ES6+ classes.
-
Use flows. Keep in mind, however, that by default
yield
in a generator would return something typed asany
. Also, don't forget to useflowResult
.*flow() { // This is not so good: `data` would have type `any`. const data = yield transport.request(); // Consider doing this: const data: ActualBackendDto = yield transport.request(); }
-
You should separate the concerns and follow SRP.
-
As such, separate pure data objects with minimal business logic footprint and actual stores.
-
Always dispose of reactions.
-
Prefer computed when possible.
Most SVG icons should be converted to a React component, either manually or via a webpack loader. An exception to this would be something like flag icons, which usually should be displayed on demand.
TODO: release process, manual testing.