Последовательная нумерация сносок с помощью React

Я пытаюсь создать компонент сноски для моей новой версии моего блога на React, но пока не могу сделать правильную нумерацию.

Я применяю в основном ту же логику, что и для своей версии Веб-компоненты того же компонента блога:

  1. Рассчитайте текущий индекс сносок, исходя из того, сколько сносок вы можете document.querySelectorAll(".footnote").
  2. Используйте этот индекс для тега <sup>, где должна появиться сноска.
  3. Добавьте указатель и содержимое сноски в контейнер сносок в конце сообщения.

Я думаю, что это не сработает из-за последовательности рендеринга, которую использует 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>

Не используйте этот возрастающий антишаблон вообще. Идентификаторы должны быть уникальными и постоянными, поскольку они становятся частью URL-адреса. Если порядок некоторого содержимого в разметке страницы изменится, то использование сгенерированной целочисленной последовательности нарушит существующие ссылки на сноски — например. ссылка, которая ранее указывала на содержимое «сноски 2», может указывать на содержимое «сноски 5».

jsejcksn 30.06.2024 08:26

Да, в идеале мне надо сделать идентификаторы непоследовательными. Но это также вопрос того, что я что-то редактирую, в конце концов, я также могу редактировать заголовки, что также может привести к поломке ссылок. Я понимаю, что это проблема другого масштаба, потому что редактирование одной сноски приведет к редактированию многих других. В любом случае, идентификаторы сносок и нумерация сносок не обязательно должны иметь за собой алгоритм.

psygo 30.06.2024 13:26

Судя по вашему комментарию к ответу ниже, очевидно, что вы понимаете, что «способ React» заключается в наличии некоего централизованного источника истины, например. состояние, которое предусмотрено, например. React Context API для дочерних FootNote компонентов. В реализации этого нет ничего особенного, поэтому неясно, какое «более простое» решение вы ищете. Запрос и изменение DOM определенно не является способом решения этой проблемы в React.

Drew Reese 03.07.2024 09:40

Мой комментарий был о его хаке для общего счетчика, а не о том, что я сделал для изменения части DOM, которую я не считаю Reactish ни в каком виде, ни в какой форме. И, кстати, я использовал термин «больше React-способа», а не «React-путь», не говоря уже о том, что я не делал каких-либо всеобъемлющих предположений об этом пользователе (и я проголосовал за его ответ, потому что считал его полезным). ).

psygo 03.07.2024 13:18

Извините, я вижу, в чем заключается неясность. На самом деле у меня было два отдельных комментария. (1) ссылка на ваш комментарий о том, что вы знаете, что вам следует использовать «более React-способ», используя контекст React, и (2) ссылка на вашу текущую реализацию, запрашивающую и изменяющую DOM в вашем минимально воспроизводимом примере, что является ясным примером и установил антипаттерн React.

Drew Reese 03.07.2024 17:31

Я только что добавил версию, использующую поставщика React, но не знаю, почему список не изменяется должным образом или как это исправить.

psygo 03.07.2024 21:27

Откуда берутся данные для вашего блога? Если это JSON, не могли бы вы разместить сноски в отдельном массиве, а затем просто map над ними? @псиго

Andy 06.07.2024 12:56

В моем блоге мои статьи представляют собой просто HTML-файлы. Я импортирую веб-компонент <foot-note> вверху. Это должен был быть простой статический веб-сайт с контентом.

psygo 06.07.2024 15:20
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
8
176
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

с контекстом, без состояния

Вот моя первая попытка. Я выбрал 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.


1. Способ React Context

Один из способов решить эту проблему — использовать 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>

Ключевые изменения

  • Мы добавили сноскиRef, чтобы гарантировать, что сноски добавляются последовательно.
  • Мы изменили структуру компонента FootNote, чтобы исправить нумерацию сносок.
  • Мы исправили ссылки в FootNotesContainer.
  • Мы не использовали манипуляции с DOM!

Это должно решить проблему со сносками.

2. Генераторный способ (не рекомендуется)

Другой подход — использование функций генератора 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>

Ключевые изменения

  • Мы больше не используем previousFootnotes.length для вычисления значения footNoteLink. Вместо этого мы используем функции useFootnoteId иgenerId.
    • Примечание. Мы удалили чтение DOM, поэтому такие действия, как удаление сносок, могут потребовать дополнительных действий.
  • Мы реализовали функцию очистки в FootNotesContainer, чтобы избежать создания ненужных сносок из-за чрезмерного повторного рендеринга.

Теперь сноски должны работать так, как вы хотите.

Другие вопросы по теме