5 ms·
Can you show the code of the "Favorites section"? Because to me it does not sound like it is as simple to update as "Just put a reference in the template". Beca
by JonathanBeuys 3y ago
Can you show the code of the "Favorites section"? Because to me it does not sound like it is as simple to update as "Just put a reference in the template". Because what is in the users favorites is probably defined in some other data structure.
Which is what I witness in real life: In theory it sounds cool to just use data in the template and everything gets updated automatically. But in reality it is a mess with a million edge cases. Worse then just calling a list of update functions when the title changes.
- jitl 3y agoHere'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