Sitemap
Shreyas manolkar

Shreyas manolkar

Founder of Conception Labs, building scalable SaaS, AI-powered products, and growth systems that turn ideas into measurable business outcomes.

Building Notion Clone: Part 2 — Crafting the Front-end Experience

14 min readJul 26, 2023

--

Press enter or click to view image in full size
Photo by Rubaitul Azad on Unsplash

Welcome to the second part of our journey toward developing a Notion clone! In the previous blog post, we discussed the architectural principles and implementation challenges of the backend. In this article, we will focus on frontend development using React with TypeScript, along with Redux and React Query, to create a powerful and interactive user interface.

Building a Notion-Like Text Editor

Rich text editors, such as Google Docs, have revolutionized the way we edit and format documents. The notion takes this a step further by allowing different types of content, including images, videos, toggles, and even entire pages, to be seamlessly integrated and organized through drag-and-drop functionality. However, appreciating the complexity behind a pleasant rich text editor can be challenging.

Most users simply want an editor that produces HTML-like documents with basic formatting options like bold, italics, and strikethrough. They also want the ability to compose paragraphs, lists, and tables and incorporate images and videos. Even editing plain text involves subtleties that editors like Google Docs and Notion handle for users.

To develop a custom text editor similar to Notion with support for various content types and dragging and dropping functionality, we will utilize ProseMirror as the primary technical component. ProseMirror is a collection of open-source JavaScript libraries designed to handle the complexities of rich text editing within web browsers.

ProseMirror abstracts many inconsistencies found in browser technology, such as content editable, which browsers heavily rely on for text editing. Interpreting input events like paste, key presses, and selections can vary across different browsers, even in modern ones like Google Chrome.

ProseMirror provides straightforward primitives to work with, such as a document represented by a node, a selection using a selection class, and changes to the document represented by transforms and transactions.

Furthermore, ProseMirror handles the rendering of updates to a rich text editor document in the DOM. Whether the document changes due to user input or remote collaboration, ProseMirror ensures that the DOM reflects the latest state of the document.

In ProseMirror, everything is represented as a node, including the top-level document. Each top-level document node contains one or more children, each of which can have its children and attributes.

For example, a simple paragraph node with the text “Hello, world” in bold and italics can be represented as follows:

COPY

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"type": { "type": "string", "const": "paragraph" },
"content": {
"type": "array",
"items": {
"type": "object",
"oneOf": [
{
"properties": {
"type": { "type": "string", "const": "text" },
"text": { "type": "string" },
"marks": {
"type": "array",
"items": {
"type": "object",
"oneOf": [
{ "properties": { "type": { "type": "string", "const": "bold" } } },
{ "properties": { "type": { "type": "string", "const": "italic" } } }
]
}
}
}
},
{
"properties": {
"type": { "type": "string", "const": "paragraph" },
"content": { "$ref": "#/properties/content" }
}
}
]
}
}
}
}

A document can be much more complex than this.

ProseMirror structures its main elements, such as paragraphs, headings, lists, and images, as nodes. Many nodes can have child nodes, creating a tree-like structure.

However, paragraph nodes differ in their structure. In ProseMirror, the content of a paragraph is represented as a flat sequence of inline elements, each with its own set of styles. This allows ProseMirror to track each node’s position based on character offsets, making it easy to select, find, and work with specific elements.

The DOM would codify that sentence as a tree, like this:

Press enter or click to view image in full size

In ProseMirror, however, the content of a paragraph is represented as a flat sequence of inline elements, each with its own set of styles:

Press enter or click to view image in full size

All nodes, including paragraphs, headings, images, etc., have associated features such as sizes, placeholders, and draggability. Specific nodes, like images or videos, also require an ID for media file retrieval in a larger content management system (CMS) environment.

To create a text editor similar to Notion and leverage ProseMirror, we combine it with Tiptap. Tiptap simplifies the use of ProseMirror, making the development process faster and more efficient.

In addition to basic features, we want our editor to support modern functionalities like multimedia, tables, to-do lists etc. Each page in our editor can contain different types of nodes, and our goal is to generate new combinations of nodes with various structures by utilizing different node types or arranging them in different orders.

To enforce rules regarding node types and their order, we utilize a schema definition in ProseMirror. This schema definition allows us to impose complex constraints on the document structure. It has its language, a subset of Regex, enabling us to specify rules for node adjacency, the maximum number of nodes in a specific position or row, and other fine-grained control over the document’s validity.

By leveraging ProseMirror and Tiptap, we can directly manipulate the Document Object Model (DOM) to render the editor’s content. Based on the schema defined in ProseMirror, we use various HTML tags to represent and display the content. The editor also supports hotkeys, markdown formatting, and the ability to store the editor’s content in HTML or JSON format. This capability is valuable for storing user information from different pages.

Some Cool Features

The ProseMirror library is highly modular and extensible, making it ideal for customizing and building text editors tailored to specific requirements, such as a Notion-like text editor. The notion itself offers several intriguing features, including a slash menu, resizable media elements, and enhanced tables.

Now, let’s explore demonstrations of each feature implemented to mimic Notion’s text editor.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

Asynchronous and Synchronous State Management

When managing the state of a Notion clone’s browser client, it’s important to consider components that require real-time updates. To address this, we can combine the best aspects of synchronous and asynchronous state management approaches, drawing inspiration from Redux and React Query.

Using Redux for synchronous state management provides several advantages. It offers a centralized store to hold the entire application’s state, making it easier to manage and access across components while ensuring consistency and avoiding data duplication. Redux enables us to define actions and reducers to handle state changes efficiently, resulting in accurate UI updates that provide a seamless user experience.

Redux also offers a time-travel debugging feature, allowing developers to replay and inspect previous state changes. This feature aids in debugging and understanding how the application’s state evolves, facilitating issue identification and resolution.

On the other hand, React Query addresses challenges related to handling data fetching, caching, and mutations asynchronously. It simplifies managing asynchronous data operations by providing hooks and utilities designed for these scenarios.

One significant advantage of React Query is its built-in caching mechanism, which reduces the need for redundant network requests. This improves performance by minimizing network traffic and reducing response time for subsequent data requests. Caching also enables offline functionality, allowing users to continue working with previously fetched data even without a network connection.

Additionally, React Query offers automatic data synchronization capabilities. It can update cached data whenever changes occur on the server, ensuring that the application always displays the latest information. This real-time update feature is particularly important for a Notion clone where multiple users may be collaborating and making simultaneous changes to shared documents or content.

Another advantage of React Query is its support for optimistic updates and mutations. It allows developers to update the UI optimistically before the server responds, creating a smooth and responsive user experience. In case of errors, React Query automatically rolls back the UI to the previous state, providing consistent and reliable application behavior.

By combining Redux for synchronous state management and React Query for asynchronous state management, Our notion clone can achieve efficient and scalable state management. Redux ensures a consistent and synchronized state across components, while React Query simplifies handling asynchronous data operations, including data fetching, caching, and mutations. These technologies provide improved performance, real-time updates, offline functionality, and a smoother user experience, all critical for a complex application like a Notion clone.

Authentication and Axios Interceptor

In our application, we implement authentication using an Axios interceptor. Our authentication strategy involves using both a refresh token and an access token. The server securely sets the refresh token as an HTTP-only cookie, while the access token is used to validate authentication.

Press enter or click to view image in full size

When a user logs in or authenticates for the first time, the server responds with both a refresh token and an access token. The access token has a limited validity period, typically around 15 minutes, after which it expires. To obtain a new access token, we need to send a request to the “/token” API endpoint with the refresh token.

To implement this process, we can utilize an Axios interceptor. This interceptor automatically intercepts every outgoing request and adds the necessary headers or tokens. When the response indicates an expired access token, we can use the refresh token to request a new access token.

Upon receiving a response indicating an expired access token, the interceptor sends a request to the “/token” API endpoint, passing the refresh token as a parameter. The server validates the refresh token and, if it is valid, issues a new access token. This new access token is then stored securely, such as in memory or secure browser storage, for subsequent requests.

By using this approach, we ensure that the user remains authenticated even after the access token expires. The refresh token serves as a secure and long-lasting authentication token, while the access token is used for short-term validation. This strategy enhances the security of the application by reducing the exposure of the refresh token and limiting the validity of the access token.

Overall, implementing authentication in a React TypeScript application using an Axios interceptor with refresh and access tokens provides a robust and secure authentication mechanism. By automatically renewing the access token upon expiration, we can maintain a seamless and uninterrupted user experience while ensuring the security of the authentication process.

Themes

Customizing the visual appearance of an application is a popular feature that allows users to personalize their experience. In our Notion clone, we have implemented dark and light themes to cater to different user preferences. Let’s explore how these themes are implemented using SCSS and React’s Context API.

Dark and Light Themes: Our application offers two distinct themes: dark and light. The dark theme presents a sleek and modern look with dark backgrounds and contrasting light text, while the light theme provides a clean and minimalistic interface with light backgrounds and dark text. These themes are carefully designed to offer users a visually appealing experience.

Using SCSS for Styling: To define the styles for each theme, we leverage the power of SCSS (Sass). SCSS provides a flexible and modular approach to styling, making it easy to manage theme-specific styles. We create a separate SCSS file, perhaps named “_colors.scss,” where we define the styles for both the dark and light themes.

By separating the styles for each theme, we can easily toggle between them by adding or removing the appropriate class on the element.

Managing Themes with React Context: To manage the theme selection and make it available to all components, we utilize React’s Context API. We create a theme context using the createContext() function provided by React. This context holds the currently selected theme and provides it to all child components.

To set up the theme context, we create a theme provider component. Inside this component, we define the initial theme state, such as ‘light’, and provide a function to toggle between themes. We wrap the application’s components with the ThemeProvider to ensure the theme context is accessible throughout the component tree.

Applying the Theme to Components: To apply the selected theme styles to individual components, we consume the ThemeContext in those components. By accessing the theme value from the context, we can dynamically add the appropriate theme class to the root element or any other elements that require theme-specific styles.

With this implementation, users can switch between dark and light themes manually in the settings tab or choose to follow their system settings for a consistent experience across different parts of the application.

Press enter or click to view image in full size
Press enter or click to view image in full size

Responsive Design

Responsive design is an indispensable and fundamental aspect of modern web and application development. In today’s digital landscape, where users access websites and applications on a myriad of devices with varying screen sizes, responsive design ensures a seamless and user-friendly experience. By adapting layouts and content dynamically, it allows information to be presented in an optimized manner, regardless of the device being used.

Responsive design principles emphasize fluidity and flexibility in design elements. This approach enables websites and applications to automatically adjust and reorganize themselves to fit different screen resolutions and orientations. Whether it’s a large desktop monitor, a laptop, a tablet, or a smartphone with a smaller screen, responsive design allows content to be presented in a visually appealing and accessible manner.

Implementing responsive design involves a combination of techniques and tools that enable developers to create interfaces that cater to a diverse range of devices. These techniques include using media queries to detect the screen size and applying appropriate styles; employing flexible grid systems to ensure responsive layouts; and utilizing scalable images and fonts to adapt to different resolutions.

By embracing responsive design, developers can provide users with an exceptional and consistent experience across devices. Whether users are accessing a website or application on their desktop computer at work, their tablet while traveling, or their smartphone on the go, the content will be accessible, engaging, and optimized for their specific device.

However, implementing responsive design in certain scenarios can present challenges. For example, in the case of a Notion clone where complex models may need to be displayed, ensuring a responsive layout can be more intricate. Models that contain intricate structures, nested elements, or multi-column layouts can be trickier to adapt to smaller screen widths.

Despite these challenges, achieving a truly responsive layout for a Notion clone is possible by implementing clever strategies. By considering viewport dimensions and using intelligent techniques such as prioritizing essential elements, reflowing content, or employing collapsible sections, developers can overcome complexities and provide users with a responsive experience.

Unsplash API

Integrating the Unsplash API repository in a Notion clone app greatly enhances the user experience when it comes to selecting and displaying page covers. By utilizing the Unsplash API, developers can provide users with a seamless way to search for images based on specific topics and preferences.

When a user wants to set a cover image for a page, they can access the search feature in the Notion clone app. Behind the scenes, the app sends a request to the Unsplash API, passing the desired topic or keyword as a parameter. The API then returns a set of images matching the search query.

Press enter or click to view image in full size

The app can present these images to the user, allowing them to browse through the results and make a selection. Once the user chooses an image, the app fetches the image URL from the API response and displays it as the page cover.

To enhance the user experience further, the app can implement additional features such as pagination or infinite scrolling, allowing users to explore more images related to their search. Additionally, incorporating options like sorting by relevance, popularity, or date can provide users with greater control over their image selection.

By leveraging the Unsplash API repository in a Notion clone app, users can easily find and select visually appealing images for their page covers. This integration enhances the app’s functionality and offers a visually captivating experience to users, similar to the original Notion application.

Uploading Images

Handling image uploads, whether for page covers or by dragging images into the text editor, can be accomplished efficiently using Firebase Storage. By leveraging Firebase Storage, developers can seamlessly handle the storage and retrieval of user-uploaded images in their applications.

When a user uploads an image for a page cover or drags an image into the text editor, the application can initiate an upload process to Firebase Storage. This involves creating a unique file name or ID for the image and sending the image file to Firebase Storage via an API call.

Press enter or click to view image in full size

Firebase Storage provides secure and scalable cloud storage, ensuring that the

Uploading images is safe and readily accessible. It also offers features like automatic scalability, handling large file sizes, and managing access controls to protect user-uploaded content.

Once the image is successfully uploaded to Firebase Storage, the application can retrieve the image’s URL or download the link. This URL can then be associated with the page cover or embedded in the text editor, allowing the image to be displayed within the application.

Additionally, Firebase Storage offers advanced features such as image resizing, thumbnail generation, and image transformation. These features can be utilized to optimize images for different devices and screen resolutions, improving the overall performance and user experience of the application.

By utilizing Firebase Storage for image uploads, we can focus on building the user interface and functionality of the application while leaving the image storage and management to a reliable cloud infrastructure. This integration ensures that the application can seamlessly handle image uploads, providing users with a smooth and efficient experience when working with page covers or dragging images into the text editor.

Conclusion:

In this article, we discussed building a Notion clone using React with TypeScript, focusing on front-end development. We discussed the complexities of creating a custom text editor, integrating ProseMirror, utilizing Tiptap for creating an efficient custom text editor, and scalable state management using Redux and React Queries. Authentication is crucial, and an Axios interceptor with refresh and access tokens ensures user authentication.

Themes are implemented using SCSS and React’s Context API, allowing users to personalize their experience. The Unsplash API and Firebase Storage are also integrated for image selection and uploads. By implementing these techniques and leveraging frameworks and libraries, a powerful and interactive Notion clone can be created, showcasing the power of React and TypeScript in enhancing the user experience.

--

--

Shreyas manolkar
Shreyas manolkar

Written by Shreyas manolkar

Founder of Conception Labs, building scalable SaaS, AI-powered products, and growth systems that turn ideas into measurable business outcomes.