Scoping comments to views and routes

Show only the comments relevant to the current view in a single-page app

By default, Pindrop shows all comments on the page at once. In a single-page app where multiple views share the same URL, comments left on one view would show up on another. Scopes fix this.

A scope is a piece of metadata attached to a comment when it's created. When the scope is no longer active, the comment is hidden. When it becomes active again, the comment reappears.

How it works

Two options work together:

  • getScope(element) — called when a user clicks to drop a pin. Return an identifier for the current view so the comment knows where it belongs.
  • isScopeActive(scope) — called for every comment on every render. Return true if the scope is currently visible, false to hide the comment.

If a comment has no scope (created before you added scopes, or on a page without getScope), it is always shown.

Next.js App Router

Use the current pathname as the scope:

typescript
'use client'
import { usePathname } from 'next/navigation'
import { useEffect, useRef } from 'react'
import { Pindrop } from 'pindrop.js'
 
export function PindropProvider() {
  const pathname = usePathname()
  const pindrop = useRef<ReturnType<typeof Pindrop.init> | null>(null)
 
  useEffect(() => {
    pindrop.current = Pindrop.init({
      storageKey: 'my-app',
      getScope: () => ({ route: pathname }),
      isScopeActive: (scope) => scope.route === pathname,
    })
    return () => pindrop.current?.destroy()
  }, [])
 
  // Tell Pindrop to re-evaluate scopes on route change
  useEffect(() => {
    pindrop.current?.refresh()
  }, [pathname])
 
  return null
}

React Router

typescript
import { useLocation } from 'react-router-dom'
import { useEffect, useRef } from 'react'
import { Pindrop } from 'pindrop.js'
 
export function PindropProvider() {
  const location = useLocation()
  const pindrop = useRef<ReturnType<typeof Pindrop.init> | null>(null)
 
  useEffect(() => {
    pindrop.current = Pindrop.init({
      storageKey: 'my-app',
      getScope: () => ({ route: location.pathname }),
      isScopeActive: (scope) => scope.route === location.pathname,
    })
    return () => pindrop.current?.destroy()
  }, [])
 
  useEffect(() => {
    pindrop.current?.refresh()
  }, [location.pathname])
 
  return null
}

Tabs and modals

Scopes aren't limited to routes — use them for any conditional UI state like tabs or modals:

typescript
Pindrop.init({
  storageKey: 'my-app',
  getScope: (element) => {
    const tab = element.closest('[data-tab]')?.getAttribute('data-tab')
    return tab ? { tab } : null
  },
  isScopeActive: (scope) => {
    return document.querySelector(`[data-tab="${scope.tab}"]`) !== null
  },
})

Comments dropped inside a tab panel are hidden when that tab is inactive and reappear when it's opened again.

Calling refresh()

Pindrop re-evaluates scope visibility automatically when comments are added or the toolbar is interacted with. For route changes or programmatic UI updates, call refresh() manually to trigger a re-render:

typescript
pindrop.refresh()

This is a fast in-memory pass — safe to call on every route change.

Last updated March 29, 2026