I'm trying to get the width of an element but I get an error when I invoke a function from the container component Sidebar
: onClick={() => setActiveComponent(sidebarViews[1])}
. Why is this error occurring?
Sidebar.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>
)
}
PropertyList
import Property from './property.tsx';
import { useCallback, useEffect, useRef, useState } from 'react';
function PropertyList() {
const ref = useRef<HTMLHeadingElement>(null);
useEffect(() => {
console.log('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
I get this error:
Error handled by React Router default ErrorBoundary: Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
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 ```
I'm trying to set activeComponent
to the PropertyList
component to render it and get the width.
The issue here is that you are passing functions to the useState
hook and state updater functions which are being interpreted as lazy initialization functions (in the case of useState(PropertyList);
) and functional state updates (in the case of setActiveComponent(sidebarViews[0])
).
When you pass functions in either of the above cases React will invoke the function and pass the returned result value for the state. This is a problem because in React we never manually/directly call our React functions. The issue is that doing this is calling any internal React hooks outside the React component lifecycle.
On the initial render the PropertyList
component is rendered which calls a useEffect
hook. When the state is updated to render the Filters
component, it appears no new hooks are called and there is now one less React hook called, thus producing the error.
You could update the logic to use functions that return the React component you wish to store in the state (versus the result of calling the React function). We basically replace the state type from JSX.Element
to React.ComponentType
, and use functional updates.
Example:
Sidebar
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>
);
}
Navigation
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>
);
}
An alternative solution would be to store the key that represents the component you wish to render at runtime and compute the component.
const componentsMap = {
Filters,
PropertyList,
};
Sidebar
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>
);
}
Navbar
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>
);
}