mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 13:17:57 +00:00
Added a first round of docs for front end (#1246)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Additional resources",
|
||||
"position": 3,
|
||||
"position": 10,
|
||||
"collapsible": true,
|
||||
"collapsed": true,
|
||||
"className": "navbar-sub-menu"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"label": "Frontend",
|
||||
"position": 3,
|
||||
"collapsible": false,
|
||||
"collapsible": true,
|
||||
"collapsed": false,
|
||||
"className": "navbar-sub-menu"
|
||||
}
|
||||
151
docs/docs/developer/frontend/best-practices.mdx
Normal file
151
docs/docs/developer/frontend/best-practices.mdx
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_custom_props:
|
||||
icon: TbChecklist
|
||||
---
|
||||
|
||||
# Best practices
|
||||
|
||||
## State management
|
||||
|
||||
We use React and Recoil for state management.
|
||||
|
||||
### Use `useRecoilState` to store state
|
||||
|
||||
We recommend that you create as many atoms as you need to store your state.
|
||||
|
||||
Rule of thumb : It's better to be using too many atoms than trying to be too concise with props drilling.
|
||||
|
||||
```tsx
|
||||
export const myAtomState = atom({
|
||||
key: 'myAtomState',
|
||||
default: 'default value',
|
||||
});
|
||||
|
||||
export function MyComponent() {
|
||||
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={myAtom}
|
||||
onChange={(e) => setMyAtom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Do not use `useRef` to store state
|
||||
|
||||
We do not recommend using `useRef` to store state.
|
||||
|
||||
If you want to store state, you should use `useState` or `useRecoilState`.
|
||||
|
||||
We recommand seeing [how to manage re-renders](#managing-re-renders) if you feel like you need `useRef` to prevent some re-renders from happening.
|
||||
|
||||
## Managing re-renders
|
||||
|
||||
Re-renders can be hard to manage in React.
|
||||
|
||||
We provide you with some rules that we follow to avoid unnecessary re-renders.
|
||||
|
||||
Keep in mind that re-renders can **always** be avoided by understanding the cause of the re-render.
|
||||
|
||||
### Work at the root level
|
||||
|
||||
We made it easy for you to avoid re-renders in new features by taking care of eliminating them at the root level.
|
||||
|
||||
There's only one `useEffect` in the sidecar component `AuthAutoRouter` that is holding all the logic that should be executed on a page change.
|
||||
|
||||
That way you know that there's only one place that can trigger a re-render.
|
||||
|
||||
### Always think twice before adding `useEffect` in your codebase
|
||||
|
||||
Re-renders are often caused by unnecessary `useEffect`.
|
||||
|
||||
You should think whether the useEffect is really needed, or if you can move the logic in a event handler function.
|
||||
|
||||
You'll find it generally easy to move the logic in a `handleClick` or `handleChange` function.
|
||||
|
||||
You can also find them in libraries like Apollo : `onCompleted`, `onError`, etc.
|
||||
|
||||
### Use a sibling component to extract useEffect or data fetching logic
|
||||
|
||||
If you feel like you need to add a `useEffect` in your root component, you should consider extracting it in a sidecar component.
|
||||
|
||||
The same can be applied for data fetching logic, with Apollo hooks.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, will cause re-renders even if data is not changing,
|
||||
// because useEffect needs to be re-evaluated
|
||||
export function PageComponent() {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
const [someDependency] = useRecoilState(someDependencyState);
|
||||
|
||||
useEffect(() => {
|
||||
if(someDependency !== data) {
|
||||
setData(someDependency);
|
||||
}
|
||||
}, [someDependency]);
|
||||
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<PageComponent />
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, will not cause re-renders if data is not changing,
|
||||
// because useEffect is re-evaluated in another sibling component
|
||||
export function PageComponent() {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
export function PageData() {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
const [someDependency] = useRecoilState(someDependencyState);
|
||||
|
||||
useEffect(() => {
|
||||
if(someDependency !== data) {
|
||||
setData(someDependency);
|
||||
}
|
||||
}, [someDependency]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<PageData />
|
||||
<PageComponent />
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Use recoil family states and recoil family selectors
|
||||
|
||||
Recoil family states and selectors are a great way to avoid re-renders.
|
||||
|
||||
They are especially useful when you need to store a list of items.
|
||||
|
||||
### You shouldn't use `React.memo(MyComponent)`
|
||||
|
||||
We do not recommend `React.memo()` usage because it does not solve the cause of the re-render, but instead breaks the re-render chain, which can lead to unexpected behavior and make the code really hard to refactor.
|
||||
|
||||
### Limit `useCallback` or `useMemo` usage
|
||||
|
||||
They are often not necessary and will make the code harder to read and maintain for a gain of performance that is unnoticeable.
|
||||
|
||||
|
||||
|
||||
10
docs/docs/developer/frontend/design-system.mdx
Normal file
10
docs/docs/developer/frontend/design-system.mdx
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
sidebar_custom_props:
|
||||
icon: TbPaint
|
||||
---
|
||||
|
||||
# Design System
|
||||
|
||||
We rely on our internal and custom design system, that is built on top of styled-components.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
sidebar_position: 4
|
||||
sidebar_custom_props:
|
||||
icon: TbFolder
|
||||
---
|
||||
|
||||
27
docs/docs/developer/frontend/getting-started.mdx
Normal file
27
docs/docs/developer/frontend/getting-started.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
sidebar_custom_props:
|
||||
icon: TbZoomQuestion
|
||||
---
|
||||
|
||||
# Getting started
|
||||
|
||||
## Installation
|
||||
|
||||
To install the front end, you'll have to [install Yarn](https://yarnpkg.com/getting-started/install) first.
|
||||
|
||||
Then just run :
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
## Development flow
|
||||
|
||||
You can start the application for local development with :
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then go to our [best-practice](/developer/frontend/best-practices) guide and the [folder architecture](/developer/frontend/folder-architecture) to learn more about how to structure your feature.
|
||||
24
docs/docs/developer/frontend/hotkeys.mdx
Normal file
24
docs/docs/developer/frontend/hotkeys.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
sidebar_custom_props:
|
||||
icon: TbKeyboard
|
||||
---
|
||||
|
||||
# Hotkeys
|
||||
|
||||
You can intercept any hotkey combination and execute a custom action.
|
||||
|
||||
We added a thin wrapper on top of [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) to make it more performant and to avoid unnecessary re-renders.
|
||||
|
||||
We also created a wrapper hook `useScopedHotkeys` to make it easy to manage scopes.
|
||||
|
||||
```ts
|
||||
useScopedHotkeys(
|
||||
'ctrl+k,meta+k',
|
||||
() => {
|
||||
openCommandMenu();
|
||||
},
|
||||
AppHotkeyScope.CommandMenu,
|
||||
[openCommandMenu],
|
||||
);
|
||||
```
|
||||
52
docs/docs/developer/frontend/overview.mdx
Normal file
52
docs/docs/developer/frontend/overview.mdx
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
sidebar_custom_props:
|
||||
icon: TbEyeglass
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
## Tech Stack
|
||||
|
||||
We took care of having a clean and simple stack, with minimal boilerplate code.
|
||||
|
||||
**App**
|
||||
|
||||
- [React](https://react.dev/)
|
||||
- [Apollo](https://www.apollographql.com/docs/)
|
||||
- [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
|
||||
- [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
**Testing**
|
||||
|
||||
- [Jest](https://jestjs.io/)
|
||||
- [Storybook](https://storybook.js.org/)
|
||||
|
||||
**Tooling**
|
||||
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- [Craco](https://craco.js.org/docs/)
|
||||
- [ESLint](https://eslint.org/)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Routing
|
||||
|
||||
We use [React Router](https://reactrouter.com/) for routing.
|
||||
|
||||
To avoid unnecessary [re-renders](/developer/frontend/best-practices#managing-re-renders) we handle all the routing logic in a `useEffect` in `AuthAutoRouter`.
|
||||
|
||||
### State Management
|
||||
|
||||
We use [Recoil](https://recoiljs.org/docs/introduction/core-concepts) for state management.
|
||||
|
||||
See our [best practices](/developer/frontend/best-practices#state-management) for more managing state.
|
||||
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io/) for unit testing and [Storybook](https://storybook.js.org/) for component testing.
|
||||
|
||||
Jest is mainly used for testing utility functions, and not components themselves.
|
||||
|
||||
Storybook is used for testing the behavior of isolated components, as well as displaying our [design system](/developer/frontend/design-system).
|
||||
102
docs/docs/developer/frontend/style-guide.mdx
Normal file
102
docs/docs/developer/frontend/style-guide.mdx
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
sidebar_custom_props:
|
||||
icon: TbPencil
|
||||
---
|
||||
|
||||
# Style guide
|
||||
|
||||
We define here the rules to follow when writing code.
|
||||
|
||||
Our goal is to have a consistent codebase, easy to read and easy to maintain.
|
||||
|
||||
For this we prefer to tend towards being a bit more verbose than being too concise.
|
||||
|
||||
Always keep in mind that code is read more often than it is written, especially on an open source project, where anyone can contribute.
|
||||
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by our linters.
|
||||
|
||||
## React
|
||||
|
||||
### Use functional components
|
||||
|
||||
Always use TSX functional components.
|
||||
|
||||
Do not use default import with const, because it's harder to read and harder to import with code completion.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, harder to read, harder to import with code completion
|
||||
const MyComponent = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
|
||||
export default MyComponent;
|
||||
|
||||
// ✅ Good, easy to read, easy to import with code completion
|
||||
export function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
Create the type of the props and call it `OwnProps` if there's no need to export it.
|
||||
|
||||
Use props destructuring.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, no type
|
||||
export function MyComponent(props) {
|
||||
return <div>Hello {props.name}</div>;
|
||||
};
|
||||
|
||||
// ✅ Good, type
|
||||
type OwnProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function MyComponent({ name }: OwnProps) {
|
||||
return <div>Hello {name}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Use nullish-coalescing operator `??`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, can return 'default' even if value is 0 or ''
|
||||
const value = process.env.MY_VALUE || 'default';
|
||||
|
||||
// ✅ Good, will return 'default' only if value is null or undefined
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
```
|
||||
|
||||
### Use optional chaining `?.`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
onClick && onClick();
|
||||
|
||||
// ✅ Good
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
### Use type instead of Interface
|
||||
|
||||
We decided to always use type instead of interface, because they almost always overlap, and type is more flexible.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
interface MyInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
type MyType = {
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'Twenty - Documentation',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
tagline: 'Twenty is cool',
|
||||
favicon: 'img/logo-square-dark.ico',
|
||||
|
||||
// Prevent search engines from indexing the doc for selected environments
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
TbBugOff,
|
||||
TbBrandVscode,
|
||||
TbFolder,
|
||||
TbEyeglass,
|
||||
TbZoomQuestion,
|
||||
TbPaint,
|
||||
TbChecklist,
|
||||
TbKeyboard,
|
||||
TbPencil,
|
||||
} from "react-icons/tb";
|
||||
|
||||
|
||||
@@ -53,6 +59,13 @@ export default function DocSidebarItemLink({
|
||||
'TbBrandVscode': TbBrandVscode,
|
||||
'TbDeviceDesktop': TbDeviceDesktop,
|
||||
'TbFolder': TbFolder,
|
||||
'TbEyeglass': TbEyeglass,
|
||||
'TbZoomQuestion': TbZoomQuestion,
|
||||
'TbPaint': TbPaint,
|
||||
'TbChecklist': TbChecklist,
|
||||
'TbKeyboard': TbKeyboard,
|
||||
'TbChecklist': TbChecklist,
|
||||
'TbPencil': TbPencil,
|
||||
};
|
||||
|
||||
let IconComponent = customProps && customProps.icon ? icons[customProps.icon] : TbFaceIdError;
|
||||
|
||||
Reference in New Issue
Block a user