Я пытаюсь создать компонент сноски для моей новой версии моего блога на React, но пока не могу сделать правильную нумерацию.
Я применяю в основном ту же логику, что и для своей версии Веб-компоненты того же компонента блога:
document.querySelectorAll(".footnote")
.<sup>
, где должна появиться сноска.Я думаю, что это не сработает из-за последовательности рендеринга, которую использует React. Есть ли обходной путь? Может быть, есть метод выбора запроса, позволяющий брать только элементы перед определенным элементом?
Вот пример того, что у меня есть на данный момент:
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>
Я также пытался сделать это более реактивным способом, но я не знаю, почему список, предоставляемый контекстом, не копируется/изменяется последовательно:
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.info(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>
🤔 А знаете ли вы, что...
Синтаксис JavaScript схож с синтаксисом языка программирования Java, но они не связаны.
с контекстом, без состояния
Вот моя первая попытка. Я выбрал React useRef
, потому что мы должны быть в состоянии достичь цели с помощью одного рендеринга. Всякий раз, когда поставщик выполняет рендеринг, он сбрасывает свою локальную ссылку. По мере рендеринга дочерних элементов провайдера ref обновляется.
function App() {
return <div>
<p>
Hello <Footnote children = "hello" />.
<Child />
World <Footnote children = "world" />.
</p>
<Footnotes />
</div>
}
function Child() {
return <span>More text from the child
<Footnote children = "more" />
and a little more <Footnote children = "and more"/>.
</span>
}
Как видите, это работает для компонентов Footnote
, которые появляются на разных уровнях дерева компонентов. Одним из предостережений этого подхода является то, что список сносок должен отображаться после всех отдельных компонентов Footnote
—
Hello [1].More text from the child[2]and a little more [3].World [4].
[1]: hello
[2]: more
[3]: and more
[4]: world
Мы можем реализовать Footnote
и Footnotes
как -
function Footnote(props) {
const context = React.useContext(Context)
const id = context.add(props.children)
return <sup>[{id}]</sup>
}
function Footnotes() {
const context = React.useContext(Context)
return <ul>
{context.get().map((children, index) => (
<li key = {index}>
[{index + 1}]: {children}
</li>
))}
</ul>
}
А Context
и Provider
-
const defaultContext = {
get: () => [],
add: () => {
throw Error(`add called outside of context`)
}
}
const Context = React.createContext(defaultContext)
function Provider(props) {
const footnotes = React.useRef([])
footnotes.current = []
const add = children => footnotes.current.push(children)
const get = () => footnotes.current
return <Context.Provider
children = {props.children}
value = {{ add, get }}
/>
}
Запустите приведенный ниже фрагмент, чтобы проверить результат в своем браузере:
const defaultContext = {
get: () => [],
add: () => {
throw Error(`add called outside of context`)
}
}
const Context = React.createContext(defaultContext)
function Provider(props) {
const footnotes = React.useRef([])
footnotes.current = []
const add = children => footnotes.current.push(children)
const get = () => footnotes.current
return <Context.Provider
children = {props.children}
value = {{ add, get }}
/>
}
function Footnote(props) {
const context = React.useContext(Context)
const id = context.add(props.children)
return <sup>[{id}]</sup>
}
function Footnotes() {
const context = React.useContext(Context)
return <ul>
{context.get().map((children, index) => (
<li key = {index}>
[{index + 1}]: {children}
</li>
))}
</ul>
}
function App() {
return <div>
<p>
Hello <Footnote children = "hello" />.
<Child />
World <Footnote children = "world" />.
</p>
<Footnotes />
</div>
}
function Child() {
return <span>More text from the child
<Footnote children = "more" />
and a little more <Footnote children = "and more"/>.
</span>
}
ReactDOM.createRoot(document.querySelector("#app")).render(
<Provider>
<App />
</Provider>
)
<script crossorigin src = "https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src = "https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id = "app"></div>
Я оставлю вам возможность создавать настоящие a
элементы и соответствующие href
. Чтобы URL-адреса оставались неизменными, я бы посоветовал не использовать номер сноски как часть URL-адреса.
Улучшением может быть возврат компонента из функции контекста add
, чтобы компонент Footnote
не брал на себя эту проблему -
function Provider(props) {
const add = children => {
const id = footnotes.current.push(children)
const fragment = children.split(" ").join("-")
return <a href=`#${encodeURIComponent(fragment)}`>${id}</a>
return // ...
}
function Footnote(props) {
const context = React.useContext(Context)
const link = context.add(props.children) // <a href=...>...</a>
return <sup>[{link}]</sup>
}
Если вы обнаружите недостатки этого подхода, поделитесь, и я постараюсь их устранить ^-^
Я не знаю, почему @NephiBalinski удалил свой ответ, но здесь он просто для справки, так как я подумал, что он полезен. По сути, он использует смесь моего второго фрагмента и того, что в итоге придумал @Mulan.
Один из способов решить эту проблему — использовать React Context. Вот исправленный и обновленный код с использованием React Context:
const FootnotesContext = React.createContext(null);
function FootnotesProvider({ children }) {
const [footnotes, setFootnotes] = React.useState([]);
const footnotesRef = React.useRef([]);
function addFootnote(html) {
footnotesRef.current = [...footnotesRef.current, html];
setFootnotes([...footnotesRef.current, ]);
}
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();
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 + 1}`} id = {`footnote-${i}`}>
{i + 1}
</a>:
<p>{f}</p>
</div>
))}
</div>
);
}
function FootNote({ html }) {
const { footnotes, addFootnote, totalFootnotes } = useFootnotes();
// const footnoteRef = React.useRef(null);
const [index, setIndex] = React.useState(0)
React.useEffect(() => {
addFootnote(html);
}, []);
React.useEffect(() => {
setIndex(footnotes.indexOf(html) + 1)
}, [footnotes])
return (
<sup
className = "footnote-sup"
// ref = {ref => footnoteRef.current = ref}
>
<a
href = {`#footnote-${index}`}
id = {`footnote-sup-${index}`}
>
{index}
</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>
Это должно решить проблему со сносками.
Другой подход — использование функций генератора ES6. Вот пример базовой функции генератора:
function* genId() {
var index = 0;
while (true)
yield index++;
}
}
let gen = genId();
console.info(gen.next().value); // 0
console.info(gen.next().value); // 1
console.info(gen.next().value); // 2
Вот исправленный и обновленный код:
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* generateId() {
let idCounter = 1;
while (true) {
yield idCounter++;
}
}
const gen = generateId();
function useFootnoteId() {
const [id, setId] = React.useState(0);
React.useEffect(() => {
setId(gen.next().value);
}, [gen]);
return id;
}
function FootNote({ html }) {
const footNoteLink = useFootnoteId();
<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>
<script src = "https://cdn.jsdelivr.net/npm/[email protected]/runtime.min.js"></script>
<div id = "root"></div>
Теперь сноски должны работать так, как вы хотите.