I’m trying to create a footnote component for my new React version of my blog, but, so far, I’m not able to get the numbering right.
I’m applying basically the same logic I did for my Web Components version of the same blog component:
- Calculate the current footnote index based on how many footnotes you can
document.querySelectorAll(".footnote")
. - Use that index for the
<sup>
tag where the footnote should appear. - Append the footnote index and content to a footnotes container at the end of the post.
I think this won’t work due to the rendering sequence React uses. Is there a workaround? Maybe there’s a query selector method for taking only elements before a certain element?
Here’s an example of what I have so far:
function Article() {
return (
<article>
<p>The<FootNote html="Footnote 1" /> article content here<FootNote html="Footnote 2" />.</p>
<FootNotesContainer />
</article>
)
}
function FootNotesContainer() {
return (
<div id="footnotes-container">
<h2>Footnotes</h2>
</div>
)
}
function FootNote({ html }) {
const [footNoteLink, setFootNoteLink] = React.useState("")
React.useEffect(() => {
const previousFootnotes =
document.querySelectorAll(".footnote")
const nextFootNoteNumber = previousFootnotes.length
setFootNoteLink(nextFootNoteNumber.toString())
const footNoteContent = document.createElement("div")
footNoteContent.innerHTML = /* html */ `
<a
href="#footnote-base-${nextFootNoteNumber}"
id="footnote-${nextFootNoteNumber}"
className="no-underline"
>${nextFootNoteNumber}</a>:
${html}
`
const footNoteContainer = document.querySelector(
"#footnotes-container"
)
footNoteContainer.appendChild(footNoteContent)
}, [html])
return (
<sup className="footnote">
<a
href={`#footnote-${footNoteLink}`}
id={`footnote-base-${footNoteLink}`}
className="text-orange-400 no-underline"
>
{footNoteLink}
</a>
</sup>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Article />
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I’ve also tried doing it in a more Reactish way, but I don’t know why the list being provided by the context isn’t being copied/mutated sequentially:
const FootnotesContext = React.createContext(null)
function FootnotesProvider({ children }) {
const [footnotes, setFootnotes] = React.useState([])
function addFootnote(html) {
setFootnotes([...footnotes, html])
}
function totalFootnotes() {
return footnotes.length
}
return (
<FootnotesContext.Provider value={{ footnotes, addFootnote, totalFootnotes }}>
{children}
</FootnotesContext.Provider>
)
}
function useFootnotes() {
const context = React.useContext(FootnotesContext)
if (!context) throw new Error("`useFootnotes` must be used within a `FootnotesProvider`.")
return context
}
function Article() {
return (
<FootnotesProvider>
<article>
<p>The
<FootNote html="Footnote 1" />{" "}
article content here
<FootNote html="Footnote 2" />.
</p>
<hr/>
<FootNotesContainer />
</article>
</FootnotesProvider>
)
}
function FootNotesContainer() {
const { footnotes } = useFootnotes()
console.log(footnotes)
return (
<div id="footnotes-container">
<h2>Footnotes</h2>
<h3>Total Footnotes: {footnotes.length}</h3>
{
footnotes.map((f, i) => (
<div className="footnote" key={i}>
<a
href={`#footnote-sup-${i}`}
id={`footnote-${i}`}
>{i + 1}</a>:
<p>{f}</p>
</div>
))
}
</div>
)
}
function FootNote({ html }) {
const { footnotes, addFootnote, totalFootnotes } = useFootnotes()
const i = totalFootnotes() + 1
React.useEffect(() => {
addFootnote(html)
}, [])
return (
<sup className="footnote-sup">
<a
href={`#footnote-${i}`}
id={`footnote-sup-${i}`}
>
{i}
</a>
</sup>
)
}
ReactDOM.createRoot(document.getElementById("root")).render(<Article />)
.footnote {
display: flex;
gap: 4px;
align-items: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
2
Answers
The React Context way
One way to solve this is using React Context. Here is the revised and updated code using React Context:
Key changes
FootNotesContainer
.This should fix the footnotes issue.
The generator way (not recommended)
Another approach is using ES6 generator functions. Here is an example of a basic generator function:
Learn more about generator functions here.
Here is the revised and updated code:
Key changes
We no longer use
previousFootnotes.length
to calculate whatfootNoteLink
is. Instead we useuseFootnoteId
andgenerateId
functions.Note: We’ve removed DOM reads, so actions like deleting footnotes may require extra steps.
We implemented a cleanup function in
FootNotesContainer
to avoid unnecessary footnote creation due to excessive re-renders.Now footnotes should work the way you want them to.
with context, without state
Here’s my first attempt at it. I chose React
useRef
because we should be able to accomplish the goal with a single render. Whenever the provider renders, it resets its local ref. As the provider’s children render, ref is updated.As you can see this works for
Footnote
components that appear at different levels in the component tree. One caveat to this approach is the footnotes list must be rendered after all individualFootnote
components –We can implement
Footnote
andFootnotes
as –And the
Context
andProvider
–Run the snippet below to verify the result in your own browser –
I’ll leave it to you to create actual
a
elements and generate the correspondinghref
. To ensure URLs stay the same, I would advise not using the footnote’s number as part of the URL.An improvement may be to return a component from the context’s
add
function so theFootnote
component does not take on this concern –If you find drawbacks to this approach, please share and I’ll try to address them ^-^