September 12, 2026
Kazi Ehsan Aziz
I want to share a very particular scenario I faced when building a multi-document rich text editor using Tiptap in a Next.js application and some React practices that I learned from it.
Let's begin by looking at the user interface to make things more clear.

In this application, a Turn is basically a rich text document. The top row shows it is possible to navigate between several Turns using the arrow buttons. This is what I meant by multi-document. The text body shown belongs to the currently selected Turn (Turn 3 in this screenshot). This is the editor part.
Now, let's look at the component boundaries of the three components that make up this interface.

The Turns component has a cache of turns/documents. It renders a TurnCard component which is
responsible for rendering some metadata about the currently shown Turn (author name,
word count, etc.) and the editor component TurnEditForm.
export function Turns({
turns
}: {
turns?: PostTurnDto[];
}) {
// A lookup cache of turns already fetched, keyed by turn number.
// Deliberately a ref and not state: nothing renders from it.
// It is populated from the `turns` prop inside a `useEffect`,
// but that is not shown here for brevity.
const turnsCacheRef = useRef<Record<number, PostTurnDto>>({});
// The state that renders the currently displayed Turn.
const [selectedTurn, setSelectedTurn] = useState<PostTurnDto>(turns[0]);
return (
<TurnCard
turn={selectedTurn}
/>
)
}
The Turns component also has logic that handles the arrow buttons which changes
the selectedTurn, code not shown here for brevity. When selectedTurn updates,
the goal is to make sure TurnCard successfully shows data for the current
selectedTurn.
Now, we can just use setSelectedTurn to change the currently selected Turn, and
that alone does cause TurnCard to rerender, since selectedTurn is passed into
its turn prop. However:
TurnCard renders TurnEditForm, which is a React Hook Form wrapping
the Tiptap component (the actual editor). And Tiptap
seeds its text from its source (React Hook Form's state)
once, when the editor instance is mounted, and never again. This behavior of
Tiptap is actually useful in my application since I have a debounced autosave
feature in TurnEditForm that frequently propagates text updates back up to the
Turns component's main cache turnsCacheRef, but the explanation of the
benefit is beyond the scope of this article. The bottomline is switching a Turn
by changing selectedTurn from Turns does rerender TurnCard and show changed
metadata appropriately, but the editor part keeps showing the previous Turn's text.
So, I need a way to tear down the editor and remount it.TurnCard has a few React states
of its own that are not reset when the component rerenders. And within the
TurnCard component, for reasons that are also outside the scope of this article,
I did not use useEffect with a dependency on turn prop to reset those states.So, rerendering TurnCard on a prop change was not enough in my case.
Remount using the key prop, as highlighted in the next code block. A key is how
you throw a component away and start over. Most
React code only ever meets key in lists. But changing any component's key destroys
that component and builds a fresh one in its place.
export function Turns({
turns
}: {
turns?: PostTurnDto[];
}) {
// ...
// The state that renders the currently displayed Turn.
const [selectedTurn, setSelectedTurn] = useState<PostTurnDto>(turns[0]);
return (
<TurnCard
key={selectedTurn.id}
turn={selectedTurn}
/>
)
}
What if you run into a case where you are already using some sort of id for a key,
like we used selectedTurn.id, and you need to remount the component on a particular
event/trigger but your id does not change during that event. Well, React only cares
if the value of key changes. So, use a compound value that combines your id with
another string. It's called a seedEpoch and the TurnCard component uses this exact
pattern when rendering TurnEditForm to satisfy another use-case in my app.
export function TurnCard({
turn
}: {
turn: PostTurnDto;
}) {
// a state that you can control explicity from any event
const [seedEpoch, setSeedEpoch] = useState(0);
return (
<TurnEditForm
key={`${turn.id}:${seedEpoch}`}
turn={turn}
/>
)
}
You can setSeedEpoch from any event/trigger and have full control over the
remounting of your component. In fact, your use-case may not involve any ids
at all and you could just use a seedEpoch on its own.