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.
- 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 }, });
- 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, });
- 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]);
- 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: ID!) { user(id: $id) { ...UserFields } } ${UserFragment} `;
- 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> ); };
- 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);
- 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>; };
- 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);
- 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} />; };
- 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, }, }); }, });