4 ms·
Here's pseudocode of the favorite section. The "SpaceView" is a database record that represents a specific user's "view" of a shared workspace. It has a few UUI
by jitl 3y ago
Here's pseudocode of the favorite section. The "SpaceView" is a database record that represents a specific user's "view" of a shared workspace. It has a few UUID[] list columns, one of which is the "bookmarked pages" list of Block UUIDs. Methods like `.getBookedmarkedPagesStore()` get a reference "RecordStore" object to a specific column inside the record. Reading the data from a RecordStore in a component implicitly subscribes the component to changes to that column. To render lists in general, we use a <List> component. It manages drag-and-drop behavior and efficient updates, react keys, etc.
export function SidebarUserContent(props: {
spaceViewStore: SpaceViewStore
}) {
const { spaceViewStore } = props
const environment = useEnvironment()
const sharedPages = useComputedStore(
() =>
getUserSharedPages({
spaceViewStore,
user: environment.currentUser,
}),
[environment.currentUser, spaceViewStore]
)
return (
<>
<SidebarSection
canCreatePage={false}
title={<FavoritesTitle />}
list={spaceViewStore.getBookmarkedPagesStore()}
/>
<SidebarTeamsSection
list={spaceViewStore.getJoinedTeamsStore()}
/>
<SidebarSection
canCreatePage={false}
title={<SharedTitle />}
orderByList={spaceViwStore.getSharedPagesOrderStore()}
orderByItems={sharedPages}
/>
<SidebarSection
title={<PrivatePagesTitle />}
canCreatePage={true}
list={spaceViewStore.getPrivatePagesStore()}
/>
</>
)
}
function SidebarSection(props: {
list?: RecordStore<Array<RecordId<BlockTable>>>
orderByList?: RecordStore<Array<RecordId<BlockTable>>>
orderByItems: Array<BlockStore>
title: React.ReactNode
canCreatePage: boolean
}) {
const { title, canCreatePage, ...listProps } = props
return (
<div style={SidebarSectionStyle}>
<SidebarTitle
actions={canCreatePage ? <CreatePageButton /> : undefined}
>
{title}
</SidebarTitle>
<List {...listProps}>
{blockStore => <SidebarPageItem store={blockStore} />}
</List>
</div>
)
}
Here's the pseudocode for the main editor view. The document itself is a "Page" block database record. We render editable text with the <Text> component. We listen to contentEditable document changes, and reconcile the mutations with the rendered <Text> components to understand where the user typed and how to update our database records in response to that activity.
All updates to database records happen inside a call to `transactionActions.createAndCommit`. Under the hood, this:
1. Builds a list of commands to send to our server, to persist the user's edit to the database. Once the edits are applied on the server, we notify collaborators those records have changed, so the collaborators can pull new data and re-render their own views.
2. Optimistically updates the local, in-memory version of the database records to reflect the change commands.
3. Schedule rendering views and recomputing derivations of the views/derivations that read from the changed records.
function PageViewBlock(props: { store: BlockStore }) {
const handleFavoritePage = useCallback(() => {
transactionActions.createAndCommit(transaction => {
const spaceViewStore = GlobalAppStore.state.currentSpaceViewStore
listActions.append(
transaction,
spaceViewStore.getBookmarkedPagesStore(),
props.store.id,
)
})
}, [props.store])
return (
<ContentEditableRoot>
<Text
style={PageVieBlockTitleStyle}
store={props.store.getTitleStore()}
/>
<FavoritePageButton onClick={handleFavoritePage} />
<List list={props.store.getContentStore()}>
{blockStore => <DraggableBlockListItem store={blockStore} />}
</List>
</ContentEditableRoot>
)
}
function ContentEditableRoot(props: { children: React.ReactNode }) {
const [element, setElement] = React.useState<HTMLDivElement | null>(null)
useMutationObserver(element, mutations => {
transactionActions.createAndCommit(transaction => {
const edits = getEditsToBlocks(mutations)
textActions.applyEdits(transaction, edits)
})
})
return <div contentEditable={true} ref={setElement}>
{props.children}
</div>
}
Our React components "just" render the state of our database rows, but because we use a generic auto-tracking state system and publish change events, our views respond in near-real-time to both local and remote data changes. A new hire engineer at Notion can create a new collaborative feature on their first day with end-to-end reactivity "just" by a) rendering part of the record in a React component, and b) updating that data in the record with transactionActions.createAndCommit.
If you're still curious about our data model, I wrote a blog post about it: https://www.notion.so/blog/data-model-behind-notion https://www.notion.so/blog/data-model-behind-notion