by Phenomenon.Studio
This is a Next.js project bootstrapped with create-next-app.
Table of contents:
- Next.js - React Framework
- React.js - UI library
- Typescript - Static type checker
- CLSX - classnames utility
- Tanstack Query - Asynchronous state management
- Axios - HTTP client
- Zod - Schema validation
- React Hook Form - Form management
- Eslint - Code linter
- Prettier - Code formatter
- Husky - commands execution handler on git events
- Install Node.js;
Require Node.js v18 or >=v20 (hydrogen as minimum)
- Install the NPM dependencies by running
npm ci; - You should create
.env.localand add variables. You can look in .env.local.example file; - Update project metadata:
- Title (/app/layout.tsx metadata constant)
- Description (/app/layout.tsx metadata constant)
- Favicons
.ico- addfavicon.iconfile in the/appfolder.svg- addicon.svgfile in the/appfolder.png- addapple-icon.pngfile in the/appfolderDocs: https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
- Run the local dev server at
localhost:3000:npm run dev - Build your production site to
./.next/:npm run build - Preview your build locally, before deploying at
localhost:3000!(should be free, no replacement to the closest port):npm run start - Check your CSS for errors and warnings:
npm run lint:stylelint - Check your code formatting:
npm run lint:prettier - Fix your code formatting:
npm run lint:prettier:fix - Check your code all together:
npm run lint - Install husky:
npm run prepare
API requests are created globally in the root of the project to be used inside API hooks. API request are not directly called in project, only in hooks.
API requests should be located inside src/api folder.
API requests are performed with some library like ky, axios etc. Based on the library, src/api folder should contain the appropriate file @ky.ts or @axios.ts. This file should contain all instances for all origins.
Example:
// @axios.ts
export const http = axios.create(...);
export const httpPrivate = axios.create(...);API requests should:
- be separated to files based on its scope.
- Example: users requests ->
users.ts; forms requests ->forms.ts
- Example: users requests ->
When Tanstack Query is used, queryClient entity is created once on project start, and is used within all the application. By setting it in global api folder, we will be able to use it wherever needed in the app.
The query client configuration file should be located at src/tanstackQuery/@queryClient.ts and include configuration as follows as bare minimum:
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
mutations: {
retry: 1,
},
},
});This configuration should be passed to <QueryClientProvider /> in src/main.tsx file.
NOTE: This configuration as allowed to be used wherever using
useQueryClienthook is not allowed:
- routes loaders
- functions may include api logic (setting query data etc.)
Contexts are optional for the root of the project and components among all the project.
No matter, where the contexts will appear, they should:
- Have separate
contextsfolder inside the folder where the hooks will be used- Global contexts will be used in all the project, should be located at
src/contextsfolder. NOTE: Any component is allowed to call such contexts. - If context will be used inside single component exclusively, you should create
contextsfolder inside the component folder. Example:src/components/ArticleCard/contexts. NOTE: such contexts are not allowed to be used outside of the component scope where the hooks folder were created. If such case appears, then you should move the hook(s) into global hooks folder. The child components (src/components/ArticleCard/components/*) only are allowed to use the context inside
- Global contexts will be used in all the project, should be located at
Each context should:
- Be created inside the
contextsfolder - Have pascal case name, ending with
<contextName>Context(example:AuthContext.tsx) - NOTE: The context file name should match the context name inside the file
// src/contexts/AuthContext.tsx
const AuthContext = createContext(...);Stores are optional for the root of the project. Current rules are applied for zustand stores
Stores are allowed to use in all the project.
Stores should:
- Have separate root
src/storesfolder
Each store should:
- Have camel case name, ending with
<storeName>Store(example:authStore.tsx) - NOTE: The store file name should match the store hook name name inside the file
<storeName>Store.ts->use<StoreName>Store.ts
// src/stores/authStore.ts
export const useAuthStore = create(...)Hooks are optional for the root of the project and components among all the project.
No matter, where the hooks will appear, they should:
- Have separate
hooksfolder inside the folder where the hooks will be used- Global hooks will be used in all the project, should be located at
src/hooksfolder - If hook will be used inside single component exclusively, you should create
hooksfolder inside the component folder. Example:src/components/ArticleCard/hooks. NOTE: such hooks are not allowed to be used outside of the component scope where the hooks folder were created. If such case appears, then you should move the hook(s) into global hooks folder
- Global hooks will be used in all the project, should be located at
Each hook should:
- Be created inside the
hooksfolder - Have camel case name, starting with
use(example:useHavePermissions.ts) - NOTE: The hook file name should match the hook name inside the file
// src/hooks/useHavePermissions.ts
export const useHavePermissions = () => {...}Because of using Tanstack query, and its hooks mechanic, following the TkDodo's recommendations, all API requests should be inside custom hooks that call useQuery and useMutation hooks. API requests were described in the relevant section above.
API hooks should be located inside src/tanstackQuery folder.
API hooks should:
- be named for the api file.
src/api/users.ts->src/tanstackQuery/users.ts - contain all hooks for every function declared in the api requests file
Single API hook should:
- be named for the api request function.
<requestName>->use<RequestName>- Example:
submitForm->useSubmitForm
- Example:
Query hooks can have the parameters to be passed like pagination, search params etc. These parameters should be passed into hooks as arguments. Recommended to pass the arguments as list of arguments, not as the object.
Query keys should be defined as described in Query keys section.
Example:
export const useGetBooks = (search: string) => {
return useQuery({
queryKey: BOOKS_QUERY_KEYS.listWithParams({ search })
// ...
})
}
export const useGetBooksByAuthorName = (authorName: string, search: string) => {
return useQuery({
queryKey: BOOKS_QUERY_KEYS.itemByAuthor(authorName, { search })
// ...
})
}It is also recommended to manage query keys in appropriate way to use them inside project.
First things first, you should create the constant that includes queryKeys:
// src/tanstackQuery/books.ts
export const BOOKS_QUERY_KEYS = {
all: ['books'] as const,
list() {
return [...BOOKS_QUERY_KEYS.all, 'list'] as const
},
listWithParams(params: { search: string }) {
return [...BOOKS_QUERY_KEYS.list(), params] as const
}
// ...
}NOTE: Query keys contacts are allowed to be used in all the project to make invalidations and prefetched possible on a lot of events occur by user activities.
And apply this in:
- Query hooks:
export const useGetBooks = (search: string) => { return useQuery({ queryKey: BOOKS_QUERY_KEYS.listWithParams({ search }) // ... }) }
- Query options:
export const getBooksQueryOptions = (search: string) => { return queryOption({ queryKey: BOOKS_QUERY_KEYS.listWithParams({ search }) // ... }); } export const useGetBooks = (search: string) => { return getBooksQueryOptions(search); }
- Query invalidations:
import { BOOKS_QUERY_KEYS } from '@/tanstackQuery/books'; queryClient.invalidateQueries({ queryKey: BOOKS_QUERY_KEYS.list() })
- Query prefetches:
import { BOOKS_QUERY_KEYS } from '@/tanstackQuery/books'; queryClient.prefetchQuery({ queryKey: BOOKS_QUERY_KEYS.list() }) // or queryClient.getQueryData({ queryKey: BOOKS_QUERY_KEYS.list() })
Mutation hooks from useMutation return the callable function as result, so no need to pass the arguments into hook call. But everything can happen to pass initial arguments into hook body directly for query client logic or whatever.
// src/tanstackQuery/books.ts
export const addBookToFavorites = (bookId: string) => {...}
// src/tanstackQuery/books.ts
import { addBookToFavorites } from '@/tanstackQuery/books';
export const useAddBookToFavorites = () => {
return useMutation({
mutationFn: addBookToFavorites
// ...
})
}
// somewhere
import { useAddBookToFavorites } from '@/tanstackQuery/books';
// ...
const { mutate: addBookToFavorites } = useAddBookToFavorites();
// ...
addBookToFavorites(bookId, {...})Utility functions are optional for the root of the project and components among all the project.
No matter, where the utils will appear, they should:
- Have separate
utilsfolder inside the folder where the utils will be used- Global utils will be used in all the project, should be located at
src/utilsfolder - If util will be used inside single component exclusively, you should create
utilsfolder inside the component folder. Example:src/components/ArticleCard/utils. NOTE: such utils are not allowed to be used outside of the component scope where the utils folder were created. If such case appears, then you should move the util(s) into global utils folder
- Global utils will be used in all the project, should be located at
Each util should:
- Be created inside the
utilsfolder - Have camel case name (example:
getHasPermissions.ts) - NOTE: The util file name should match the util name inside the file
- (Optional): Unit tests can be written for the util
<utilName>.ts-><utilName>.test.ts
//getHasPermissions.ts
export const getHasPermissions = () => {...}Constants are optional for the root of the project and components among all the project.
There are 2 types of constants to use:
- Regular constants (
constants.ts) - Schemas constants (
schemas/folder)
The rules described below are applied for both of them. The only difference is:
constants.ts- for regular constants like time tokens, regexps etc.schemas/folder - forzodschemas will be used in other schemas in all the project
No matter, where the constants will appear, they should:
- Have separate
constants.tsfile inside the folder where the constants will be created- Global constants will be used in all the project, should be located at
src/constants.tsfile - If constants will be used inside single component exclusively, you should create
constants.tsfile inside the component folder. Example:src/components/ArticleCard/constants.ts.
NOTE: such constants are not allowed to be used outside of the component scope where the constants file were created. If such case appears, then you should move the constants(s) into global constants file
- Global constants will be used in all the project, should be located at
Schemas should:
- Have separate
schemasfolder inside the folder where the schemas will be used- Global schemas will be used in all the project, should be located at
src/schemas/folder - If schemas will be used inside single component exclusively, you should create
schemas/folder inside the component folder. Example:src/components/ArticleCard/schemas/.
- Global schemas will be used in all the project, should be located at
- Each schema should have camel case name with ending
<schemaName>Schema.ts. - Each schema should have its inferred type
Few more thing should be applied to schemas:
import { z } from 'zod';
// ...
export const signUpSchema = z.object({...});
export type SignUpSchema = z.infer<typeof signUpSchema>;Types are optional for the root of the project and components among all the project.
The root project types should include:
- Generic global types
- Global primitive types for several components
The components types should include:
- Component props
- Components props partitions
Styles are optional for the root of the project and components among all the project.
The global styles are located inside src/styles folder
This folder should include:
index.css- root project styles (incl. imports of other root style files described below)reset.css- predefined browsers styles reset filevariables.css- (optional) global variables file. This file can be created if there are a lot of variables to create and manage them easily. In case of ~25 variables they can still be maintained inindex.css.fonts.css- (optional) global fonts to be implemented through@font-facedirective.
Components should be located at:
src/ui- basic primitive components (Example: buttons, typography, wrappers etc.)
- do not have complex logic (complex hooks, contexts)
- can NOT use
src/componentscomponents inside
src/components- complex components use
src/uicomponents inside as building blocks - Can have any types of hooks, contexts inside
- complex components use
The component should:
- Have separate folder
- Have pascal case name (example:
ButtonorArticleCard) - Have default export of the component itself
The component folder should contain:
index.tsx- the component JSX, entry points of componentstyles.module.css- the styles of component file (optional)
NOTE: If component has to haves hooks/utils/constants/contexts, take a look at relevant chapters above.
Modules are core blocks are used for routing. Router entries render modules only. It is not allowed to pass the components from src/components or src/ui.
Modules are located at src/modules folder.
Modules represent pages we should display within router. Modules hierarchy may also represent the routes subrouting.
Every module should:
- be named for the route it represent:
http://localhost:3000/about->src/modules/About
- have the same architecture as
src/componentsorsrc/uias described above - have no props
- module name should match the module component name:
// src/modules/About/index.tsx export const About: React.FC = () => {...}
Modules are allowed:
- to use
src/componentsand/orsrc/uicomponents inside - to have own hooks
- to have own constants/schemas/styles
- to have own sub-modules and/or sub-components (
src/modules/About/components/...) - to use its sub-modules inside if it is not a sub-route
Submodules are the modules inside the some module (src/modules/About/components/...).
Submodules may have everything regular modules can have and do, but they can be used in two ways:
- as sub-component for the rot module
- but it is already not allowed to be used as sub-route
- as sub-route:
src/modules/About/components/Settings->http://localhost:3000/about/settings
- Collect all icons as separate files with
.svgextension and kebab-case naming. BUT the size of the icon should be added to the end of file name, starting with_underscore
Example:
src
├── icons
│ ├── arrow-left_16.svg
│ ├── search_20.svg
│ └── arrow-right-circle_24.svg- Update all dynamic color elements of each icon to be filled/stroked with
currentColorvalue. it will inherit the CSScolorrule applied to the icon ot its parent - Import the icon as regular JSX component:
import CloseCircleFill16Icon from '@/icons/close-circle-fill_16.svg'; // ... return ( // ... <CloseCircleFill16Icon className={s.icon} /> );