Inspired by BulletProof React, I applied its codebase architecture concepts to the Twenty codebase.
This article focuses only on the state management in Twenty codebase.
Prerequisite
- State management in Twenty codebase — Part 1.0
Approach
The approach we take is simple:
Pick a route, for example, {id}.twenty.com/settings/profile
Locate this route in Twenty codebase.
Review how the state is managed.
We repeat this process for 3+ pages to establish a common pattern, see if there’s any exceptions.
In this part 1.1, you will learn about the state management in the settings/general route for a workspace. We will find out what libraries Twenty used, how the files are structured, how the data flows to manage its state.
I reviewed the settings/profile route, I found that the following components give us a clear picture about the state management.
Please note that there are other components in this SettingsProfile component that make up the below route, but we are analyzing 3 components in this route.
SettingsProfile
SettingsProfile.tsx has 98LOC and uses the following components to render the page.
WorkspaceMemberPictureUploader
NameFields
EmailField
SettingsCard
SetOrChangePassword
DeleteAccount
These are individual components that are rendered in this /profile page.
WorkspaceMemberPictureUploader
WorkspaceMemberPictureUploader.tsx has 144LOC. This handles picture upload for the workspace.
The following code handles uploading the file:
...
const [uploadPicture] = useMutation(
UploadWorkspaceMemberProfilePictureDocument,
);
const { updateOneRecord } = useUpdateOneRecord();
...
const handleUpload = async (file: File) => {
if (isUndefinedOrNull(file) || !canEdit) {
return;
}
const controller = new AbortController();
setUploadController(controller);
setIsUploading(true);
setErrorMessage(null);
let newAvatarUrl: string | null = null;
try {
const { data } = await uploadPicture({
variables: { file },
context: {
fetchOptions: {
signal: controller.signal,
},
},
});
const signedFile = data?.uploadWorkspaceMemberProfilePicture;
if (!isDefined(signedFile)) {
throw new Error('Avatar upload failed');
}
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
idToUpdate: workspaceMemberId,
updateOneRecordInput: { avatarUrl: signedFile.url },
});
newAvatarUrl = signedFile.url;
if (isEditingSelf && isDefined(currentWorkspaceMember)) {
setCurrentWorkspaceMember({
...currentWorkspaceMember,
avatarUrl: newAvatarUrl,
});
}
if (isDefined(onAvatarUpdated)) {
onAvatarUpdated(newAvatarUrl);
}
setUploadController(null);
setErrorMessage(null);
} catch (error) {
const message =
error instanceof Error ? error.message : t`Failed to upload picture`;
setErrorMessage(t`An error occurred while uploading the picture.`);
enqueueErrorSnackBar({ message });
} finally {
setIsUploading(false);
}
};
Once the upload is complete, there’s a check if the file exists. So how is the state managed here?
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useAtomState(
currentWorkspaceMemberState,
);
...
newAvatarUrl = signedFile.url;
if (isEditingSelf && isDefined(currentWorkspaceMember)) {
setCurrentWorkspaceMember({
...currentWorkspaceMember,
avatarUrl: newAvatarUrl,
});
}
When you use setCurrentWorkspaceMember, this state is also available in other parts of your codebase because Twenty uses Jotai to manage state and useAtomState is a hook defined in jotai/hooks/useAtomState.
NameFields
Let’s find out how the NameFields are updated. You will find the below code in NameFields.tsx
const [firstName, setFirstName] = useState(
currentWorkspaceMember?.name?.firstName ?? '',
);
const [lastName, setLastName] = useState(
currentWorkspaceMember?.name?.lastName ?? '',
);
const { updateOneRecord } = useUpdateOneRecord();
// TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
const debouncedUpdate = useDebouncedCallback(async () => {
try {
if (!currentWorkspaceMember?.id) {
throw new Error('User is not logged in');
}
if (autoSave) {
await updateOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
idToUpdate: currentWorkspaceMember?.id,
updateOneRecordInput: {
name: {
firstName: firstName,
lastName: lastName,
},
},
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName,
lastName,
},
});
}
} catch (error) {
logError(error);
}
}, 500);
useUpdateOneRecord is commonly used here. There is a debounced update to the name fields, first and last names. The state is set again using the setCurrentWorkspaceMember defined as shown below:
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useAtomState(
currentWorkspaceMemberState,
);
This is a consistent pattern that can be reused in the other components within this route or in other settings related routes.
About me:
Hey, my name is Ramu Narasinga. Email: ramu.narasinga@gmail.com
Tired of AI slop?
I spent 3+ years studying OSS codebases and wrote 350+ articles on what makes them production-grade. I built an open source tool that reviews your PR against your existing codebase patterns.
Your codebase. Your patterns. Enforced.


Top comments (0)