Notescroll: MVP

The following article is a case-study about my progress so far on Notescroll (name t.b.d.). If you just want to check out the app, click this link.

The Idea

Every table-top gamer knows the struggle of keeping good notes for sessions and campaigns. I wanted to make something that would give players a simple tool to record their sessions and keep track of the campaign and world. Something that is simple enough to be used while playing the game, but with features that are useful for players and DMs beyond what you would find in a generic note-taking app.


  • Simple interface
  • Specific but not overly-opinionated structure
  • Scalable and extendable


  • React
  • Next
  • Supabase

Initial build

The goal for the initial build was to put together a working foundation of the app - authentication of users, creating campaigns and notes, and doing so in a way that is extendable and scalable so that adding more advanced features such as collaborative editing and references could be added without significant refactoring of the codebase.


I use React's Context API to provide the state of authentication to the entire app. The component creates the context and listens to the Supabase API for any changes to state and updates the context accordingly.

I wrap all authenticated content in an <AuthCheck> component, this ensures that everything inside can assume that the user is authenticated. The component shows a log-in page if authenticated === false.

The Editor

I went with Tiptap, a headless wrapper for Prosemirror that includes some handy extensions like support for collaboration with Y.js. Being headless, Tiptap offers no opinions on styling so it was quite simple to integrate it with the existing styling of the app.

Autosave was fairly simple to implement.

callback() gets called from Tiptap's onUpdate method, and only runs if updated === true, which signals that the component has received it's initial data from Supabase, avoiding a redundant database call.

The editor saves both JSON and plaintext, so in the future searching through documents doesn't need to parse JSON.

Now I can use this component on any page.

Tricky bits

Using Next.js for a client side app

While Next.js is best used for server-side rendering, it can also function as a client-side router with minimal configuration. When Next.js prerenders dynamic pages, it creates a [slug].html page for each dynamic route. We can use Netlify's _redirects configuration to serve the correct route for each request

This directs all traffic with a dynamic campaign query and dynamic character query to the correct dynamic route.

Next.js router

Pages that are statically generated will hydrate with an empty query object, and then are later updated with the query parameters. This means that components that rely on the URL parameters wont have access to them on first render. There are a number of ways to deal with this issue, like conditionally rendering components based on the value of router.query, but this introduces added complexity. I implemented a modified version of Lucas Nascimento's useClientRouter hook to return the query params on first render.

Request animation frame

Although useEffect is supposed to run after the browser paints to the screen this doesn't always happen which can break functionality. For example if you modify the DOM with useEffect in quick succession, you are not guaranteed two separate paints. This can break things like CSS transition animations.

In order to ensure that the browser paints before an effect is run, you can use requestAnimationFrame. This takes advantage of the event loop in Javascript.

Animation frames run before layout and style is calculated, and before the browser paints. Since events such as Timeouts are run on the other side of the event loop, this guarantees the browser calculates styles and layout, and paints before the code inside of the effect runs.