11 ошибок, которых следует избегать при создании React приложений

год назад·7 мин. на чтение

Некоторые распространенные ошибки в разработке React приложений, которых следует избегать.

Поскольку React становится все более и более популярным, все больше и больше React разработчиков сталкиваются с различными проблемами в процессе разработки. В этой статье, мы обобщим некоторые распространенные ошибки в разработке React приложений, чтобы помочь вам их избежать. Если вы только начинаете использовать React, рекомендуется внимательно ознакомиться с этой статьей. Если вы уже используете React для разработки проектов, также рекомендуется проверить и заполнить пробелы.
Прочитав эту статью, вы узнаете, как избежать эти 11 ошибок React:
  • При рендеринге списка не используется key
  • Изменение значения состояния прямым присваиванием
  • Привязка значения состояния непосредственно к свойству value инпута
  • Использование состояния сразу после выполнения setState
  • Появление бесконечного цикла при использовании useState + useEffect
  • Отсутствие очистки побочных эффектов в useEffect
  • Неправильное использование логических операторов
  • Тип пропсов компонента не типизирован
  • Передача строк в качестве значений компонентам
  • Имя компонента не начинается с заглавной буквы
  • Неверная привязка события к элементу

Ошибка: При рендеринге списка не используется key

Проблема Когда мы впервые изучали React, мы отображали список следующим образом:
const items = [
  { id: 1, value: 'item1' },
  { id: 2, value: 'item2' },
  { id: 3, value: 'item3' },
  { id: 4, value: 'item4' },
  { id: 5, value: 'item5' }
];

const listItems = items.map((item) => {
  return <li>{item.value}</li>
});
После рендеринга консоль выдаст предупреждение, что для элементов списка необходимо указать ключ. Решение Вам просто нужно последовать этой подсказке и добавить key к каждому элементу:
const items = [
  { id: 1, value: ‘item1’ },
  { id: 2, value: ‘item2’ },
  { id: 3, value: ‘item3’ },
  { id: 4, value: ‘item4’ },
  { id: 5, value: ‘item5’ }
];

const listItems = items.map((item) => {
  return <li key={item.id}>{item.value}</li>
});
key помогает React определить, какие элементы были изменены, например, добавлены или удалены. Поэтому нам нужно установить уникальное значение ключа для каждого элемента в массиве. Для значения ключа лучше всего установить уникальное значение. В приведенном выше примере используется id. Можно использовать индекс массива, но такой подход не рекомендуется. Уникальный ключ помогает React следить за изменениями списка - какой элемент удалился или переместился.

Ошибка: Изменение значения состояния прямым присваиванием

Проблема В React нельзя назначать состояние и изменять напрямую, иначе это вызовет проблемы.
// классовый компонент

handleChange = () => {
   this.state.name = "John";
};
В этот момент будет выдано предупреждение не изменять состояние напрямую, а использовать setState(). Решение Классовые компоненты могут быть изменены с помощью setState(), а функциональные компоненты могут быть изменены с помощью useState():
// Классовые компоненты: используйте setState()
this.setState({ name: "John" });

// Функциональные компоненты:используйте useState()
const [name, setName] = useState("");
setName("John");

Ошибка: Привязка значения состояния непосредственно к свойству value инпута

Проблема Когда мы напрямую привязываем значение состояния к свойству value инпута, мы обнаружим, что независимо от того, что мы вводим в поле ввода, содержимое поля ввода не изменится.
export default function App() {
  const [count, setCount] = useState(0);
  return <input type="text" value={count} />;
}
Это связано с тем, что мы используем переменную состояния в качестве значения по умолчанию для присвоения значения <input>, а состояние в функциональном компоненте может быть изменено только функцией set*, возвращаемым useState. Таким образом, решение также очень простое, просто используйте функцию set* при изменении. Подробнее о том как работать с инпутом в React можно прочитать в этой статье. Решение Просто привяжите событие onChange к <input> и измените его, вызвав setCount:
export default function App() {
  const [count, setCount] = useState(0);
  const handleChange= (event) => setCount(event.target.value);

  return <input type="text" value={count} onChange={handleChange} />;
}

Ошибка: Использование состояния сразу после выполнения setState

Проблема Когда мы изменяем данные через setState() и сразу же хотим получить новые данные, возникнет ситуация, что возвращаются старые данные:
// Классовые компоненты

// инициализация состояния
this.state = { name: "John" };

// обновление состояния
this.setState({ name: "Hello, John!" });
console.log(this.state.name); // => John
Это связано с тем, что setState() является асинхронным. Когда setState() выполняется, реальная операция обновления будет помещена в асинхронную очередь для выполнения, а код, который будет выполняться следующим (т.е. console.log в примере), выполняется синхронно, поэтому выводимое в консоль состояние не является последним значением. Решение Просто передайте последующую операцию, которая будет выполняться как функция, в качестве второго параметра setState(), эта функция обратного вызова будет выполнена после завершения обновления.
this.setState({ name: "Hello, John!" }, () => {
  console.log(this.state.name); // => Hello, John!
});
Теперь обновленное значение выводится правильно.

Ошибка: Появление бесконечного цикла при использовании useState + useEffect

Проблема Когда мы напрямую вызываем метод set*(), возвращаемый useState() внутри useEffect(), и не устанавливаем второй параметр в useEffect(), мы столкнемся с бесконечным циклом:
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  });
  return <div className="App">{count}</div>;
}
После этого можно увидеть, что данные на странице обновляются, и функция useEffect() вызывается бесконечно, входя в состояние бесконечного цикла. Решение Это распространенная проблема неправильного использования useEffect(). useEffect() можно рассматривать как комбинацию трех функций жизненного цикла: componentDidMount, componentDidUpdate и componentWillUnmount в классовых компонентах. useEffect(effect, deps) принимает 2 аргумента:
  • effect функция, которая должна выполниться (побочный эффект)
  • deps массив зависимостей
При изменении массива deps выполняется функция эффекта. Чтобы изменить метод, вам нужно всего лишь передать [] в качестве второго аргумента useEffect() :
export default function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount(count + 1);
  }, []);

  return <div className="App">{count}</div>;
}
Приведем 4 случая использования useEffect:
  • Если второй параметр не передан: при обновлении любого состояния будет запущена функция эффекта useEffect.
useEffect(() => {
  setCount(count + 1);
});
  • Если второй параметр - это пустой массив: функция эффекта useEffect срабатывает только при монтировании и размонтировании.
useEffect(() => {
  setCount(count + 1);
}, []);
  • Если второй параметр представляет собой массив с одним значением: функция эффекта useEffect будет запускаться только при изменении значения.
useEffect(() => {
  setCount(count + 1);
}, [name]);
  • Если второй параметр представляет собой массив c несколькими значениями: функция эффекта useEffect будет запускаться при изменении хотя бы одного из значений из списка зависимостей.
useEffect(() => {
  setCount(count + 1);
}, [name, age]);

Ошибка: Отсутствие очистки побочных эффектов в useEffect

Проблема В классовых компонентах мы используем метод жизненного цикла componentWillUnmount() для очистки некоторых побочных эффектов, таких как таймеры, слушатели событий и т. д. Решение Из функции эффекта useEffect() может быть возвращена функция очистки, которая аналогична роли метода жизненного цикла componentWillUnmount():
useEffect(() => {
  // ...
  return () => clearInterval(id);
}, [name, age]);

Ошибка: Неправильное использование логических операторов

Проблема В синтаксисе JSX/TSX мы часто используем логические значения для управления отображаемыми элементами, и во многих случаях мы используем оператор && для обработки этой логики:
const count = 0;
const Comp = () => count && <h1>Chris1993</h1>;
Мы думаем, что в это время страница будет отображать пустой контент, но на самом деле на ней отобразится 0. Решение Причина в том, что ложное выражение приводит к тому, что элементы после && пропускаются, и будет возвращено значение ложного выражения. Поэтому нужно стараться написать условие оценки как можно более полным, не полагаясь на истинное и ложное логическое значение JavaScript для сравнения:
const count = 0;
const Comp = () => count > 0 && <h1>Chris1993</h1>;
Теперь страница будет отображать пустой контент, как и ожидается.

Ошибка: Типы просов компонента не типизированы

Проблема Если компоненты, разработанные разными членами команды, не имеют четко определенных типов для просов, то для коллег будет не очевидно, как использовать компоненты, например:
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
Решение
  • Определить типы пропсов компонента, используя TypeScript.
// Классовые компоненты
interface AppProps {
  value: string;
}
interface AppState {
  count: number;
}
class App extends React.Component<AppProps, AppStore> {
  // ...
}

// Функциональные компоненты
interface AppProps {
  value?: string;
}
const App: React.FC<AppProps> = ({ value = "", children }) => {
  //...
};
  • Без использования TypeScript типы пропсов могут быть определены с помощью propTypes.
const UserInfo = (props) => {
  return (
    <div>
      {props.name} : {props.age}
    </div>
  );
};
UserInfo.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
};

Ошибка: Передача строк в качестве значений компонентам

Проблема Так как React имеет шаблонный синтаксис, очень похожий на HTML, часто бывает так, что числа передаются напрямую компонентам в пропсы, что приводит к неожиданному результату:
<MyComp count="99"></MyComp>
Сравнение props.count === 99 в компоненте MyComp вернет false. Решение Правильный способ должен заключаться в использовании фигурных скобок для передачи пропсов:
<MyComp count={99}></MyComp>
Передача строковых просов будет выглядеть следующим образом:
<MyComp count={"99"}></MyComp>

Ошибка: Имя компонента не начинается с заглавной буквы

Проблема Начинающие разработчики часто забывают называть свои компоненты с заглавной буквы. Компоненты, начинающиеся со строчной буквы в JSX/TSX, компилируются в элементы HTML, такие как <div /> для тегов HTML.
class myComponent extends React.component {}
Решение Просто измените первую букву на заглавную:
class MyComponent extends React.component {}

Ошибка: Неверная привязка события к элементу в классовых компонентах

Проблема
import { Component } from "react";

export default class HelloComponent extends Component {
  constructor() {
    super();
    this.state = {
      name: "John",
    };
  }
  update() {
    this.setState({ name: "Hello John!" });
  }

  render() {
    return (
      <div>
        <button onClick={this.update}>update</button>
      </div>
    );
  }
}
При нажатии на кнопку update консоль сообщит об ошибке, что невозможно прочитать свойства undefined (чтение setState) Решение Это происходит потому, что this не привязан к тому контексту, который мы ожидаем. Есть несколько решений:
  • Привязать контекст в конструкторе при помощи метода bind
constructor() {
  super();
  this.state = {
    name: "John"
  };
  this.update = this.update.bind(this);
}
  • Использовать стрелочные функции
update = () => {
  this.setState({ name: "Hello John!" });
};
  • Привязать прямо в функции рендеринга
<button onClick={this.update.bind(this)}>update</button>
  • Использовать стрелочные функции в функции рендеринга (не рекомендуется, т.к. это создает новую функцию каждый раз при рендеринге компонента, что влияет на производительность)
<button onClick={() => this.update()}>update</button>

Полное руководство по React Router v6. Часть 2 - Продвинутые определения маршрутов

год назад·8 мин. на чтение

В этой статье рассмотрим то, как описывать роуты в React Router версии 6. Как создавать вложенные маршруты, контекст, хук useRoutes и многое другое.

Серия статей о React Router v6 состоит из 4 частей.
  1. Основы React Router
  2. Продвинутые определения маршрутов (рассматривается в этой статье)
  3. Управление навигацией
  4. Подробно о роутерах
Здесь React Router действительно становится интересным. Есть много интересных вещей, которые вы можете сделать с роутингом, чтобы сделать более сложные маршруты, более удобные для чтения и в целом более функциональные. Это можно сделать с помощью пяти основных методов.
  1. Динамическая маршрутизация
  2. Приоритет маршрутизации
  3. Вложенные маршруты
  4. Множественные маршруты
  5. Хук useRoutes

Динамическая маршрутизация

Самая простая и распространенная расширенная функция в React Router — это обработка динамических маршрутов. В нашем примере предположим, что мы хотим визуализировать компонент для отдельных книг в нашем приложении. Мы могли бы жестко закодировать каждый из этих маршрутов, но если у нас есть сотни книг или возможность для пользователей создавать книги, то невозможно жестко закодировать все эти маршруты. Вместо этого нам нужен динамический маршрут.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
</Routes>
Последний маршрут в приведенном выше примере — это динамический маршрут с динамическим параметром :id. Для определения динамического маршрута в React Router нужно поставить двоеточие перед тем, что должно изменяться. В нашем случае наш динамический маршрут будет соответствовать любому URL-адресу, который начинается с /book и заканчивается каким-либо значением. Например, /books/1, /books/bookName и /books/literally-anything будут соответствовать нашему динамическому маршруту. Почти всегда, когда у вас есть такой динамический маршрут, вы хотите получить доступ к динамическому значению в вашем пользовательском компоненте, где на помощь приходит хук useParams.
import { useParams } from "react-router-dom"

export function Book() {
  const { id } = useParams()

  return (
    <h1>Book {id}</h1>
  )
}
Хук useParams не принимает параметры и возвращает объект с ключами, соответствующими динамическим параметрам в вашем маршруте. В нашем случае нашим динамическим параметром является :id, поэтому хук useParams вернет объект, имеющий ключ id, и значение этого ключа будет фактическим идентификатором в нашем URL. Например, если бы наш URL-адрес был /books/3, наша страница отображала бы Book 3.

Приоритет маршрутизации

Когда мы имели дело только с жестко закодированными маршрутами, было довольно легко узнать, какой маршрут будет отображаться, но при работе с динамическими маршрутами это может быть немного сложнее. Возьмем, к примеру, эти маршруты.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
</Routes>
Если у нас есть URL-адрес /books/new, какой маршрут будет задействован? Технически у нас есть два маршрута, которые совпадают. И /books/:id, и /books/new. Они будут совпадать, так как динамический маршрут будет просто предполагать, что new является частью :id URL-адреса, поэтому React Router нужен другой способ определить /books/:id какой маршрут отренедрить. В старых версиях React Router маршрут, который был определен первым, будет визуализирован, поэтому в нашем случае будет отображаться маршрут /books/:id, что, очевидно, не то, что нам нужно. К счастью, версия 6 React Router изменила это подход, поэтому теперь React Router будет использовать алгоритм, чтобы определить, какой маршрут, скорее всего, является тем, который вам нужен. В нашем случае мы, очевидно, хотим отобразить /books/new, поэтому React Router выберет этот маршрут для нас. Фактический принцип работы этого алгоритма очень похож на специфику CSS, поскольку он попытается определить, какой маршрут, соответствующий нашему URL-адресу, является наиболее конкретным (имеет наименьшее количество динамических элементов), и выберет этот маршрут. Раз уж мы заговорили о приоритете маршрутизации, я также хочу поговорить о том, как создать маршрут, который соответствует всему.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
  <Route path="*" element={<NotFound />} />
</Routes>
* соответствует чему угодно, что делает его идеальным для таких вещей, как страница 404. Маршрут, содержащий *, также будет менее конкретным, чем все остальное, поэтому вы никогда случайно не сопоставите маршрут *, когда другой маршрут также совпал бы.

Вложенные маршруты

В приведенном выше примере у нас есть три маршрута, которые начинаются с /books, поэтому мы можем вложить эти маршруты друг в друга, чтобы очистить наши маршруты.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books">
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
Это вложение довольно просто сделать. Все, что вам нужно сделать, это создать родительский Route, в котором свойство path будет установлено на общий путь для всех ваших дочерних компонентов Route. Внутри родительского маршрута вы можете поместить все дочерние компоненты Route. Единственное отличие состоит в том, что проп path дочерних компонентов Route больше не включает общий маршрут /books. Кроме того, маршрут для /books заменяется компонентом Route, который не имеет проп path, но вместо этого имеет index. Все это говорит о том, что путь индексного маршрута совпадает с родительским Route. Но истинная сила вложенных маршрутов заключается в том, как они обрабатывают общие макеты.

Общие макеты (layout)

Давайте представим, что мы хотим отобразить раздел nav со ссылками на каждую книгу, а также форму для добавления книг с любой из наших страниц. Чтобы сделать это, как правило, нам нужно было бы создать общий компонент для хранения этой навигации, а затем импортировать его в каждый отдельный компонент, связанный с книгой. Однако это создает дублирование, поэтому React Router создал собственный вариант решения этой проблемы. Если вы передадите проп element родительскому маршруту, он отобразит этот компонент для каждого дочернего Route, что означает, что вы можете легко разместить общую навигацию или другие общие компоненты на каждой дочерней странице.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet />
    </>
  )
}
Наш новый код будет работать следующим образом: всякий раз, когда мы сопоставляем маршрут внутри родительского Route /book, он будет отображать компонент BooksLayout, который содержит нашу общую навигацию. Route, какой бы дочерний маршрут ни был сопоставлен, он будет отображаться везде, т.к. компонент Outlet находится внутри нашего компонента макета. Компонент Outlet — это, по сути, компонент-заполнитель, который будет отображать любое содержимое нашей текущей страницы. Эта структура невероятно полезна и делает обмен кодом между маршрутами невероятно простым. Теперь последний способ, которым вы можете поделиться макетами с React Router, - это обернуть дочерние компоненты Route в родительский Route, который определяет только проп element и не имеет пропса path.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route element={<OtherLayout />}>
    <Route path="/contact" element={<Contact />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
Этот фрагмент кода создаст два маршрута, /contact и /about, которые отображаются внутри компонента OtherLayout. Метод оборачивания нескольких компонентов Route в родительский компонент Route без пропса path полезен, если вы хотите, чтобы эти маршруты использовали один макет, даже если у них нет похожего path.

Контекст Outlet

Еще одна важная вещь, которую нужно знать о компонентах Outlet, это то, что они могут принимать проп context, который будет работать так же, как контекст React.
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet context={{ hello: "world" }} />
    </>
  )
}
import { useParams, useOutletContext } from "react-router-dom"

export function Book() {
  const { id } = useParams()
  const context = useOutletContext()

  return (
    <h1>Book {id} {context.hello}</h1>
  )
}
Как видно из этого примера, мы передаем значение контекста { hello: "world" }, а затем в нашем дочернем компоненте мы используем хук useOutletContext для доступа к значению нашего контекста. Это довольно распространенный шаблон для использования, поскольку часто у вас будут общие данные между всеми вашими дочерними компонентами, что является идеальным вариантом использования для этого контекста.

Множественные маршруты

Еще одна невероятно мощная вещь, которую вы можете сделать с помощью React Router, — это использовать несколько компонентов Routes одновременно. Это может быть сделано либо в виде двух отдельных компонентов Routes, либо в виде вложенных Routes.

Отдельные Routes

Если вы хотите отобразить два разных раздела контента, которые зависят от URL-адреса приложения, вам потребуется несколько компонентов Routes. Это очень распространенный случай, если, например, у вас есть боковая панель, на которой вы хотите отображать определенный контент для определенных URL-адресов, а также главная страница, которая должна отображать определенный контент на основе URL-адреса.
import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { BookSidebar } from "./BookSidebar"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/books">Books</Link></li>
        </ul>
      </nav>

      <aside>
        <Routes>
          <Route path="/books" element={<BookSidebar />}>
        </Routes>
      </aside>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}
В приведенном выше примере у нас есть два Routes. Основные маршруты Routes определяют все основные компоненты для нашей страницы, а затем у нас есть вторичные Routes внутри aside, которые будут отображать боковую панель для страницы наших книг, когда мы находимся в /books. Это означает, что если наш URL-адрес /books, то оба наших компонента Routes будут отображать контент, поскольку они оба имеют уникальное совпадение для /books в своих Routes. Еще одна вещь, которую вы можете сделать с несколькими компонентами Routes, — это жестко закодировать проп location.
<Routes location="/books">
  <Route path="/books" element={<BookSidebar />}>
</Routes>
Жестко кодируя проп location, мы переопределяем поведение по умолчанию React Router, поэтому независимо от того, каков URL-адрес нашей страницы, этот компонент Routes будет соответствовать своему Route, как если бы URL-адрес был /books.

Вложенные Routes

Другой способ использования нескольких компонентов Routes заключается в том, чтобы вложить их друг в друга. Это довольно распространено, если у вас много маршрутов и вы хотите очистить свой код, переместив похожие маршруты в свои собственные файлы.
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books/*" element={<BookRoutes />} />
  <Route path="*" element={<NotFound />} />
</Routes>
import { Routes, Route } from "react-router-dom"
import { BookList } from "./pages/BookList"
import { Book } from "./pages/Book"
import { NewBook } from "./pages/NewBook"
import { BookLayout } from "./BookLayout"

export function BookRoutes() {
  return (
    <Routes>
      <Route element={<BookLayout />}>
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
        <Route path="new" element={<NewBook />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  )
}
Вложенные Routes в React Router довольно просты в реализации. Все, что вам нужно сделать, это создать новый компонент для хранения вложенных Routes, этот компонент должен иметь компонент Routes, а внутри этого компонента Routes должны быть все компоненты Route, которые вы сопоставляете с родительским Route. В нашем случае мы перемещаем все наши маршруты /books в этот компонент BookRoute. Затем в родительских Routes вам нужно определить Route, который имеет путь, равный пути, разделяемому всеми вашими вложенными Routes. В нашем случае это будет /books. Однако важно то, что вам нужно заканчивать path родительского маршрута маршрутом *, иначе он не будет правильно соответствовать дочерним роутам. По сути, код, который мы написали, говорит, что всякий раз, когда маршрут начинается с /book/, он должен искать внутри компонента BookRoutes, чтобы увидеть, есть ли соответствующий Route. Вот почему у нас есть еще один маршрут * в BookRoutes, чтобы мы могли гарантировать, что если наш URL-адрес не совпадает ни с одним из BookRoutes, он правильно отобразит компонент NotFound.

Хук useRoutes

Последнее, что вам нужно знать об определении маршрутов в React Router, это то, что вы можете использовать объект JavaScript для определения ваших маршрутов вместо JSX.
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books">
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
      </Route>
    </Routes>
  )
}
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  const element = useRoutes([
    {
      path: "/",
      element: <Home />
    },
    {
      path: "/books",
      children: [
        { index: true, element: <BookList /> },
        { path: ":id", element: <Book /> }
      ]
    }
  ])

  return element
}
Эти два компонента имеют одни и те же маршруты, единственное различие заключается в том, как они были определены. Если вы все же решите, что хотите использовать хук useRoutes, все пропсы, которые вы обычно передаете компонентам Route, вместо этого просто передаются в виде пар ключ-значение объекта.