React Router обнаружил следующую ошибку во время рендеринга. Ошибка: отрисовано меньше перехватчиков, чем ожидалось

Я пытаюсь получить ширину элемента, но получаю ошибку при вызове функции из компонента контейнера Sidebar: onClick = {() => setActiveComponent(sidebarViews[1])}. Почему возникает эта ошибка?

Боковая панель.tsx

function Sidebar() {
  const [activeComponent, setActiveComponent] = useState(PropertyList);
  const sidebarViews = [<Filters/>, <PropertyList/>]; // sidebarViews : JSX.Element[]

  return (
    <div className = "sidebar">
      <div className = "sidebar-top">
        <Navigation
          sidebarViews = {sidebarViews}
          setActiveComponent = {setActiveComponent}
        />
      </div>
      <div className = "sidebar-content">
        {activeComponent}
      </div>
    </div>        
  );
}

function Navigation({
  sidebarViews,
  setActiveComponent
}: {
  sidebarViews: JSX.Element[],
  setActiveComponent: React.Dispatch<React.SetStateAction<JSX.Element>>
}) {
  return (
    <div style = {{backgroundColor: "blue", color: "white"}}>                
      <div className = "sidebar-navigation-bar">
        <NavigationButton
          onClick = {() => setActiveComponent(sidebarViews[0])}
          icon = "tune"
          key = "0"
        ></NavigationButton>
        <NavigationButton
          onClick = {() => setActiveComponent(sidebarViews[1])}
          icon = "pin_drop"
          key = "1"
        ></NavigationButton>
      </div>
    </div>
  )
}

function NavigationButton(
  { onClick, icon }: { onClick: any, icon: string }
) {
  return (
    <span onClick = {onClick} className = "material-icons">
      {icon}
    </span>
  )
}

Список свойств

import Property from './property.tsx';
import { useCallback, useEffect, useRef, useState } from 'react';

function PropertyList() {
  const ref = useRef<HTMLHeadingElement>(null);

  useEffect(() => {
    console.info('width', ref.current ? ref.current.offsetWidth : 0);
  }, [ref.current]);

  return (
    <div className = "property-list">
      <Property key = "1"></Property>
      <Property key = "2"></Property>
    </div>
  );
}

export default PropertyList

Я получаю эту ошибку:

Ошибка, обрабатываемая React Router по умолчанию ErrorBoundary: Ошибка: Отрисовано меньше крючков, чем ожидалось. Это может быть вызвано случайным ранним заявление о возврате.

DefaultErrorComponent@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:3960:15
RenderErrorBoundary@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:3992:5
DataRoutes@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:5154:7
Router@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:4421:7
RouterProvider@http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=91faa1fe:4969:7
localhost:5173:13851:25
    overrideMethod (index):13851
    overrideMethod (index):13898
    DefaultErrorComponent hooks.tsx:536
    React 14
    onClick sidebar.tsx:34
    React 23
    <anonymous> main.tsx:25 ```

Я пытаюсь установить activeComponent к компоненту PropertyList, чтобы отобразить его и получить ширину.

🤔 А знаете ли вы, что...
React обеспечивает высокую производительность благодаря эффективной обработке изменений DOM.


1
51
2

Ответы:

Это происходит потому, что вы сохраняете компонент, который хотите отобразить, в состоянии. В строгом режиме React он будет отображаться дважды, чтобы выявить ошибки, при которых хуки вызываются в разном порядке.

Попробуйте это:

SideBar.tsx

const [activeComponent, setActiveComponent] = useState<'list' | 'filters'>('list');

...

<div className = "sidebar-content">
                {activeComponent === 'list' && <ProperyList />}
                {activeComponent === 'filters' && <Filters />}
</div>

Навигация

<NavigationButton onClick = {() => setActiveComponent('filters')} icon = "tune" key = "0"></NavigationButton>

<NavigationButton onClick = {() => setActiveComponent('list')} icon = "pin_drop" key = "1"></NavigationButton>

Решено

Проблема

Проблема здесь в том, что вы передаете функции перехватчику useState и функциям обновления состояния, которые интерпретируются как функции ленивой инициализации (в случае useState(PropertyList);) и обновления функционального состояния (в случае setActiveComponent(sidebarViews[0])).

Когда вы передаете функции в любом из вышеперечисленных случаев, React вызовет функцию и передаст возвращаемое значение результата для состояния. Это проблема, потому что в React мы никогда вручную или напрямую не вызываем наши функции React. Проблема в том, что при этом вызываются любые внутренние перехватчики React вне жизненного цикла компонента React.

При первоначальном рендеринге отображается компонент PropertyList, который вызывает хук useEffect. Когда состояние обновляется для рендеринга компонента Filters, оказывается, что новые хуки не вызываются, и теперь вызывается на один крючок React меньше, что приводит к ошибке.

Решение

Вы можете обновить логику, чтобы использовать функции, которые возвращают компонент React, который вы хотите сохранить в состоянии (а не результат вызова функции React). По сути, мы заменяем тип состояния с JSX.Element на React.ComponentType и используем функциональные обновления.

Пример:

Боковая панель

function Sidebar() {
  // Initializer function that returns the React function
  const [ActiveComponent, setActiveComponent] = useState<React.ComponentType>(
    () => PropertyList
  );

  // Array of function component references
  const sidebarViews = [Filters, PropertyList]; // inferred React.ComponentType[]

  return (
    <div className = "sidebar">
      <div className = "sidebar-top">
        <Navigation
          sidebarViews = {sidebarViews}
          setActiveComponent = {setActiveComponent}
        />
      </div>
      <div className = "sidebar-content">
        {/* Render component as JSX 🙂 */}
        <ActiveComponent />
      </div>
    </div>
  );
}

Навигация

function Navigation({
  sidebarViews,
  setActiveComponent,
}: {
  sidebarViews: React.ComponentType[];
  setActiveComponent: React.Dispatch<React.SetStateAction<React.ComponentType>>;
}) {
  return (
    <div style = {{ backgroundColor: "blue", color: "white" }}>
      <div className = "sidebar-navigation-bar">
        <NavigationButton
          // Functional state update to return the React function
          onClick = {() => setActiveComponent(() => sidebarViews[0])}
          icon = "tune"
          key = "0"
        />
        <NavigationButton
          // Functional state update to return the React function
          onClick = {() => setActiveComponent(() => sidebarViews[1])}
          icon = "pin_drop"
          key = "1"
        />
      </div>
    </div>
  );
}

Альтернативным решением было бы сохранить ключ, представляющий компонент, который вы хотите отобразить, во время выполнения, и вычислить этот компонент.

const componentsMap = {
  Filters,
  PropertyList,
};

Боковая панель

function Sidebar() {
  const [activeComponent, setActiveComponent] =
    useState<keyof typeof componentsMap>("PropertyList");
  const sidebarViews: (keyof typeof componentsMap)[] = [
    "Filters",
    "PropertyList",
  ];

  // Compute the component to render
  const ActiveComponent = componentsMap[activeComponent];

  return (
    <div className = "sidebar">
      <div className = "sidebar-top">
        <Navigation
          sidebarViews = {sidebarViews}
          setActiveComponent = {setActiveComponent}
        />
      </div>
      <div className = "sidebar-content">
        {/* Render component as JSX 🙂 */}
        <ActiveComponent />
      </div>
    </div>
  );
}

Навбар

function Navigation({
  sidebarViews,
  setActiveComponent,
}: {
  sidebarViews: (keyof typeof componentsMap)[];
  setActiveComponent: React.Dispatch<
    React.SetStateAction<keyof typeof componentsMap>
  >;
}) {
  return (
    <div style = {{ backgroundColor: "blue", color: "white" }}>
      <div className = "sidebar-navigation-bar">
        <NavigationButton
          onClick = {() => setActiveComponent("Filters")}
          icon = "tune"
          key = "0"
        />
        <NavigationButton
          onClick = {() => setActiveComponent("PropertyList")}
          icon = "pin_drop"
          key = "1"
        />
      </div>
    </div>
  );
}