Files
twenty/.cursor/rules/react-state-management-guidelines.md
Marie 362d540aac Misc. of sentry improvements (#12233)
This PR mixes various initiatives to improve visibility on sentry 

**1. Catch errors on workflow jobs**
commit [catch workflowTriggerExceptions in job
handle](1dbba8c9e2)
@thomtrp 

**2. Fix type in messagingImportExceptionHandler** 
commit [fix type issue on
messagingImportExceptionHandler](919bb3844c)
@guillim 

**3. Catch invalid uuid errors thrown by Postgres by rightfully typing
expected id as uuid**
commits [use UUIDFilter instead of IDFilter to get graphqlError in case
of malformed
id](57cc315efe),
[use UUIDFilter
(2)](304553d770),
[fix ids typed as UUID instead of
ID](f95d6319cf)
@Weiko 
⚠️⚠️⚠️ when we deploy this PR we need to flush the schema types from
redis as this PR changes them ⚠️⚠️⚠️


**4. Do not group UNKNOWN errors together**
commit [do not group unknown errors
together](c299b39c8f)
Some CustomException classes have introduced UNKNOWN error codes as a
default fallback error code. We use CustomException codes to group
issues together, but we don't want to do it with UNKNOWN error as they
may not have anything in common. For exemple [this sentry for UNKNOWN
code](https://twenty-v7.sentry.io/issues/6605750776/events/a72272d8941b4fa2add9b1f39c196d3f/?environment=prod&environment=prod-eu&project=4507072499810304&query=Unknown&referrer=next-event&stream_index=0)
groups together "Unknown error importing calendar events for calendar
channel...", "Insufficent permissions...", to name a few.

**5. Improve postgres error grouping**
commit [group together postgres
errors](567c25495e)
Postgres error are thrown by typeORM as QueryFailedError. we have a lot
of them on sentry where they are badly grouped They are currently
grouped on sentry according to the stack trace, which leads them to
sometimes be grouped even if they don't have anything in common : for
exemple [this sentry for
QueryFailedError](https://twenty-v7.sentry.io/issues/6563624590/events/2d636821e27a448595b647b4b5a7d6a8/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=previous-event&sort=date&stream_index=0)
groups together "user mapping not found for "postgres" and "invalide
type for uuid: 'fallback-id'" to name a few. I attempted to improve the
grouping by grouping them with a new custom fingerPrint composed of the
[code returned by
Postgres](https://www.postgresql.org/docs/current/errcodes-appendix.html)
+ the truncated operation name (Find, Aggregate, Check...). This is
still not ideal as postgres code are quite broad - we could have the
same error code for two Find operations with different causes. let's
give this a try !
2025-05-23 13:36:02 +00:00

5.2 KiB

State Management Guidelines

Core State Management Principles

Twenty uses a combination of Recoil for global state and Apollo Client for server state management. This document outlines our state management conventions and best practices.

Global State Management

Recoil Usage

  • Use Recoil for global application state
  • Keep atoms small and focused
    // ✅ Correct
    // states/theme.ts
    export const themeState = atom<'light' | 'dark'>({
      key: 'themeState',
      default: 'light',
    });
    
    // states/user.ts
    export const userState = atom<User | null>({
      key: 'userState',
      default: null,
    });
    
    // ❌ Incorrect
    // states/globalState.ts
    export const globalState = atom({
      key: 'globalState',
      default: {
        theme: 'light',
        user: null,
        settings: {},
        // ... many other unrelated pieces of state
      },
    });
    

Atom Organization

  • Place atoms in the states/ directory
  • Group related atoms in feature-specific files
    // states/workspace/atoms.ts
    export const workspaceIdState = atom<string>({
      key: 'workspaceIdState',
      default: '',
    });
    
    export const workspaceSettingsState = atom<WorkspaceSettings>({
      key: 'workspaceSettingsState',
      default: defaultSettings,
    });
    

Server State Management

Apollo Client Usage

  • Use Apollo Client for all GraphQL operations
  • Leverage Apollo's caching capabilities
    // ✅ Correct
    const { data, loading } = useQuery(GET_USER_QUERY, {
      variables: { id },
      fetchPolicy: 'cache-first',
    });
    
    // ❌ Incorrect
    const [user, setUser] = useState(null);
    useEffect(() => {
      fetch('/api/user/' + id).then(setUser);
    }, [id]);
    

Query Organization

  • Separate operation files
  • Use fragments for shared fields
    // queries/user.ts
    export const UserFragment = gql`
      fragment UserFields on User {
        id
        name
        email
      }
    `;
    
    export const GET_USER = gql`
      query GetUser($id: UUID!) {
        user(id: $id) {
          ...UserFields
        }
      }
      ${UserFragment}
    `;
    

State Management Best Practices

Multiple Small Atoms

  • Prefer multiple small atoms over prop drilling
  • Keep atoms focused on specific features
    // ✅ Correct
    export const selectedViewState = atom<string>({
      key: 'selectedViewState',
      default: '',
    });
    
    export const viewFiltersState = atom<ViewFilters>({
      key: 'viewFiltersState',
      default: {},
    });
    
    // ❌ Incorrect - Prop drilling
    const ViewContainer = ({ selectedView, filters, onViewChange }) => {
      return (
        <ViewHeader view={selectedView} onViewChange={onViewChange}>
          <ViewContent>
            <ViewFilters filters={filters} />
          </ViewContent>
        </ViewHeader>
      );
    };
    

No useRef for State

  • Never use useRef for state management
  • Use proper state management tools
    // ✅ Correct
    const [count, setCount] = useState(0);
    // or
    const [count, setCount] = useRecoilState(countState);
    
    // ❌ Incorrect
    const countRef = useRef(0);
    

Data Fetching

  • Extract data fetching to sibling components
  • Keep components focused on presentation
    // ✅ Correct
    const UserProfileContainer = () => {
      const { data, loading } = useQuery(GET_USER);
      if (loading) return <LoadingSpinner />;
      return <UserProfile user={data.user} />;
    };
    
    const UserProfile = ({ user }: UserProfileProps) => {
      return <div>{user.name}</div>;
    };
    
    // ❌ Incorrect
    const UserProfile = () => {
      const { data, loading } = useQuery(GET_USER);
      if (loading) return <LoadingSpinner />;
      return <div>{data.user.name}</div>;
    };
    

Hook Usage

  • Use appropriate hooks for state access
  • Choose between useRecoilValue and useRecoilState based on needs
    // ✅ Correct - Read-only access
    const theme = useRecoilValue(themeState);
    
    // ✅ Correct - Read-write access
    const [theme, setTheme] = useRecoilState(themeState);
    
    // ❌ Incorrect - Using state setter when only reading
    const [theme, _] = useRecoilState(themeState);
    

Performance Considerations

Selector Usage

  • Use selectors for derived state
  • Memoize complex calculations
    // ✅ Correct
    const filteredUsersState = selector({
      key: 'filteredUsersState',
      get: ({ get }) => {
        const users = get(usersState);
        const filter = get(userFilterState);
        return users.filter(user => 
          user.name.toLowerCase().includes(filter.toLowerCase())
        );
      },
    });
    
    // ❌ Incorrect - Calculating in component
    const UserList = () => {
      const users = useRecoilValue(usersState);
      const filter = useRecoilValue(userFilterState);
      const filteredUsers = users.filter(user =>
        user.name.toLowerCase().includes(filter.toLowerCase())
      );
      return <List users={filteredUsers} />;
    };
    

Cache Management

  • Configure appropriate cache policies
  • Handle cache invalidation properly
    // ✅ Correct
    const [updateUser] = useMutation(UPDATE_USER, {
      update: (cache, { data }) => {
        cache.modify({
          id: cache.identify(data.updateUser),
          fields: {
            name: () => data.updateUser.name,
          },
        });
      },
    });