From 390e70a196d86fc1a01c01ec31e382acb252fd24 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Fri, 18 Aug 2023 00:16:48 +0200 Subject: [PATCH] Added a first round of docs for front end (#1246) --- .../docs/developer/additional/_category_.json | 2 +- docs/docs/developer/frontend/_category_.json | 2 +- .../developer/frontend/best-practices.mdx | 151 ++++++++++++++++++ .../docs/developer/frontend/design-system.mdx | 10 ++ .../frontend/folder-architecture.mdx | 2 +- .../developer/frontend/getting-started.mdx | 27 ++++ docs/docs/developer/frontend/hotkeys.mdx | 24 +++ docs/docs/developer/frontend/overview.mdx | 52 ++++++ docs/docs/developer/frontend/style-guide.mdx | 102 ++++++++++++ docs/docusaurus.config.js | 2 +- docs/src/theme/DocSidebarItem/Link/index.js | 13 ++ 11 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 docs/docs/developer/frontend/best-practices.mdx create mode 100644 docs/docs/developer/frontend/design-system.mdx create mode 100644 docs/docs/developer/frontend/getting-started.mdx create mode 100644 docs/docs/developer/frontend/hotkeys.mdx create mode 100644 docs/docs/developer/frontend/overview.mdx create mode 100644 docs/docs/developer/frontend/style-guide.mdx diff --git a/docs/docs/developer/additional/_category_.json b/docs/docs/developer/additional/_category_.json index f1d8dba14..6827d6bba 100644 --- a/docs/docs/developer/additional/_category_.json +++ b/docs/docs/developer/additional/_category_.json @@ -1,6 +1,6 @@ { "label": "Additional resources", - "position": 3, + "position": 10, "collapsible": true, "collapsed": true, "className": "navbar-sub-menu" diff --git a/docs/docs/developer/frontend/_category_.json b/docs/docs/developer/frontend/_category_.json index 7ec594c80..ade306b5b 100644 --- a/docs/docs/developer/frontend/_category_.json +++ b/docs/docs/developer/frontend/_category_.json @@ -1,7 +1,7 @@ { "label": "Frontend", "position": 3, - "collapsible": false, + "collapsible": true, "collapsed": false, "className": "navbar-sub-menu" } \ No newline at end of file diff --git a/docs/docs/developer/frontend/best-practices.mdx b/docs/docs/developer/frontend/best-practices.mdx new file mode 100644 index 000000000..162cf8e13 --- /dev/null +++ b/docs/docs/developer/frontend/best-practices.mdx @@ -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 ( +
+ setMyAtom(e.target.value)} + /> +
+ ); +} +``` + +### 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
{data}
; +}; + +export function App() { + return ( + + + + ); +} +``` + +```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
{data}
; +}; + +export function PageData() { + const [data, setData] = useRecoilState(dataState); + const [someDependency] = useRecoilState(someDependencyState); + + useEffect(() => { + if(someDependency !== data) { + setData(someDependency); + } + }, [someDependency]); + + return <>; +}; + +export function App() { + return ( + + + + + ); +} +``` + +### 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. + + + diff --git a/docs/docs/developer/frontend/design-system.mdx b/docs/docs/developer/frontend/design-system.mdx new file mode 100644 index 000000000..6cbb1620d --- /dev/null +++ b/docs/docs/developer/frontend/design-system.mdx @@ -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. + diff --git a/docs/docs/developer/frontend/folder-architecture.mdx b/docs/docs/developer/frontend/folder-architecture.mdx index a78143fe8..711453bb1 100644 --- a/docs/docs/developer/frontend/folder-architecture.mdx +++ b/docs/docs/developer/frontend/folder-architecture.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 0 +sidebar_position: 4 sidebar_custom_props: icon: TbFolder --- diff --git a/docs/docs/developer/frontend/getting-started.mdx b/docs/docs/developer/frontend/getting-started.mdx new file mode 100644 index 000000000..c8279ce30 --- /dev/null +++ b/docs/docs/developer/frontend/getting-started.mdx @@ -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. diff --git a/docs/docs/developer/frontend/hotkeys.mdx b/docs/docs/developer/frontend/hotkeys.mdx new file mode 100644 index 000000000..1ac3cc442 --- /dev/null +++ b/docs/docs/developer/frontend/hotkeys.mdx @@ -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], +); +``` diff --git a/docs/docs/developer/frontend/overview.mdx b/docs/docs/developer/frontend/overview.mdx new file mode 100644 index 000000000..6aef07777 --- /dev/null +++ b/docs/docs/developer/frontend/overview.mdx @@ -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). diff --git a/docs/docs/developer/frontend/style-guide.mdx b/docs/docs/developer/frontend/style-guide.mdx new file mode 100644 index 000000000..35c34a915 --- /dev/null +++ b/docs/docs/developer/frontend/style-guide.mdx @@ -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
Hello World
; +}; + +export default MyComponent; + +// ✅ Good, easy to read, easy to import with code completion +export function MyComponent() { + return
Hello World
; +}; +``` + +### 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
Hello {props.name}
; +}; + +// ✅ Good, type +type OwnProps = { + name: string; +}; + +export function MyComponent({ name }: OwnProps) { + return
Hello {name}
; +}; +``` + +## 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; +}; +``` + diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 2f31539a8..ba194dfb6 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -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 diff --git a/docs/src/theme/DocSidebarItem/Link/index.js b/docs/src/theme/DocSidebarItem/Link/index.js index f8a7e2c98..afedfb257 100644 --- a/docs/src/theme/DocSidebarItem/Link/index.js +++ b/docs/src/theme/DocSidebarItem/Link/index.js @@ -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;