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[];
}) {
// 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 just 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. Why that becomes useful is a bit beyond the scope
of this article, but the bottomline is switching a Turn by changing selectedTurn
from Turns does rerender TurnCard and its metadata appropriately, but the
editor 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 array containing the turn prop
to reset those states.So, rerendering TurnCard on a prop change was not enough in my case.
Remount using the key prop. A key is how you throw a component away and start
over, as highlighted in the previous code block, on line 11 of turns.tsx. 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 cannot change. 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.