mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 21:57:56 +00:00
Added a first round of docs for front end (#1246)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"label": "Additional resources",
|
"label": "Additional resources",
|
||||||
"position": 3,
|
"position": 10,
|
||||||
"collapsible": true,
|
"collapsible": true,
|
||||||
"collapsed": true,
|
"collapsed": true,
|
||||||
"className": "navbar-sub-menu"
|
"className": "navbar-sub-menu"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Frontend",
|
"label": "Frontend",
|
||||||
"position": 3,
|
"position": 3,
|
||||||
"collapsible": false,
|
"collapsible": true,
|
||||||
"collapsed": false,
|
"collapsed": false,
|
||||||
"className": "navbar-sub-menu"
|
"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:
|
sidebar_custom_props:
|
||||||
icon: TbFolder
|
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} */
|
/** @type {import('@docusaurus/types').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
title: 'Twenty - Documentation',
|
title: 'Twenty - Documentation',
|
||||||
tagline: 'Dinosaurs are cool',
|
tagline: 'Twenty is cool',
|
||||||
favicon: 'img/logo-square-dark.ico',
|
favicon: 'img/logo-square-dark.ico',
|
||||||
|
|
||||||
// Prevent search engines from indexing the doc for selected environments
|
// Prevent search engines from indexing the doc for selected environments
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import {
|
|||||||
TbBugOff,
|
TbBugOff,
|
||||||
TbBrandVscode,
|
TbBrandVscode,
|
||||||
TbFolder,
|
TbFolder,
|
||||||
|
TbEyeglass,
|
||||||
|
TbZoomQuestion,
|
||||||
|
TbPaint,
|
||||||
|
TbChecklist,
|
||||||
|
TbKeyboard,
|
||||||
|
TbPencil,
|
||||||
} from "react-icons/tb";
|
} from "react-icons/tb";
|
||||||
|
|
||||||
|
|
||||||
@@ -53,6 +59,13 @@ export default function DocSidebarItemLink({
|
|||||||
'TbBrandVscode': TbBrandVscode,
|
'TbBrandVscode': TbBrandVscode,
|
||||||
'TbDeviceDesktop': TbDeviceDesktop,
|
'TbDeviceDesktop': TbDeviceDesktop,
|
||||||
'TbFolder': TbFolder,
|
'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;
|
let IconComponent = customProps && customProps.icon ? icons[customProps.icon] : TbFaceIdError;
|
||||||
|
|||||||
Reference in New Issue
Block a user