I'm new to React and am trying to implement an HTML structure like this
<div class="heads">
<div class="head">head1</div>
<div class="head">head2</div>
<div class="head">head3</div>
</div>
<div class="bodies">
<div class="body">body1</div>
<div class="body">body2</div>
<div class="body">body3</div>
</div>
Each head-body pair (like head1-body1) belongs to a single component. Without changing the structure, how would I write such a component?
From my understanding, it needs to be "mounted" at two different points (in "heads" and in "bodies").
A practical example of such structure would be a tabbed dialog which has tab headers within the "heads" div and their contents within "bodies".
What I'm looking for is some code that would be able to translate this:
<Tabs ...>
<Tab head="head1">body1</Tab>
<Tab head="head2">body2</Tab>
<Tab head="head3">body3</Tab>
</Tabs>
into the above HTML structure. Is that possible in React?
This isn't a natural fit in React, but you do have some options.
Option 1: You could bite the bullet and render the head and body components separately, just like you'd write the HTML separately. For example, Material-UI, one of the popular React component libraries, does this for its Tabs component.
Option 2: Components are just code, so you can manipulate them however you want. (In the following example, I made up some unique IDs for each tab so that I have a key prop to keep React happy, and I'm also demonstrating dynamically adding/removing elements.)
function MyPage({hideTab3}) {
const tabs = [
{ id: 1, header: 'head1', body: <MyBody1 /> },
{ id: 2, header: 'head2', body: <MyBody2 /> },
!hideTab3 && { id: 3, header: 'head3', body: <MyBody3 /> },
].filter(Boolean);
return (
<div className="heads">
{tabs.map(({id, head}) => <div className="head" key={id}>{head}</div>)}
</div>
<div className="bodies">
{tabs.map(({id, body}) => <div className="body" key={id}>{body}</div>)}
</div>
)
}
Options 3: You could use component slots, which can be simple or quite sophisticated.
Here's an example of the more sophisticated approach - a component can examine its child components' props and handle their slots themselves. I'm using TypeScript here to help enforce the restrictions on child components.
interface TabProps {
head: ReactNode;
children: ReactNode;
}
function Tab({ head, children }: TabProps) {
return <div className="body">{children}</div>;
}
interface TabsProps {
// `children` is normally a generic `ReactNode`; specify
// a strong type here, since we manipulate child props.
children:
| ReactElement<TabProps, typeof Tab>
| ReactElement<TabProps, typeof Tab>[];
}
function Tabs({ children }: TabsProps) {
return (
<>
<ul className="heads">
{Children.map(children, (tab, i) => (
// Tab head loop: Extract out child props, as shown in
// https://sandroroth.com/blog/react-slots#slots-by-type.
//
// Production-ready should maybe throw for non-Tab children.
<li className="head" key={i}>
{tab.props.head}
</li>
))}
</ul>
{/* Bodies can show children's default renders. */}
<div className="bodies">{children}</div>
</>
);
}
export default function App() {
return (
<Tabs>
<Tab head="head1">body 1</Tab>
<Tab head="head2">body 2</Tab>
</Tabs>
);
}
Personally, I'm less likely to use this approach - as mentioned in the sandroroth.com article, it can interfere with composability (having one component inspect another's props no longer works well if you start wrapping and customizing components), and I tend to favor "explicit and composable" over "magical and elegant." But it's a perfectly valid technique.
Other options: You could try more advanced techniques, like using a portal to let a body render its heading elsewhere in the DOM - React's own portal is designed for rendering into non-React nodes, but third-party components like Material-UI's Portal may provide some ideas. Or provide a React context that lets each body register itself via useEffect to specify what tabs should be rendered. These options are more complicated and likely harder to maintain - I wouldn't go down this path unless the other options really wouldn't work for whatever reason.
Yes, you can do it easily.
Please refer below structure.
const App = () => {
const [tab, setTab] = useState("tab1");
return (
<div>
<div class="heads">
<div onClick={()=>{setTab("tab1")}} class="head">head1</div>,
<div onClick={()=>{setTab("tab2")}} class="head">head2</div>,
<div onClick={()=>{setTab("tab3")}} class="head">head3</div>
</div>
<div class="bodies">
{{
"tab1": <div class="body">body1</div>,
"tab2": <div class="body">body2</div>,
"tab3": <div class="body">body3</div>
}[tab]}
</div>
</div>
)
}
If you want dynamic tabs you can integrate like this
const App = () => {
const [tabs, setTabs] = useState([
{head: "Head 1", body: <div>body1</div>},
{head: "Head 2", body: <div>body2</div>},
{head: "Head 3", body: <div>body3</div>},
{head: "Head 4", body: <div>body4</div>},
{head: "Head 5", body: <div>body5</div>},
{head: "Head n", body: <div>bodyn</div>},
]);
const [tab, setTab] = useState(0);
return (
<div>
<div class="heads">
{
tabs.map((tab, index) => (<div onClick={()=>{setTab(index)}}>{tab.head}</div>))
}
</div>
<div class="bodies">
{tabs?.[index]?.body}
</div>
</div>
)
}
And then you will get the same structure with
<Tabs ...>
<Tab head="head1">body1</Tab>
<Tab head="head2">body2</Tab>
<Tab head="head3">body3</Tab>
</Tabs>
Just you will need to add CSS
// For example
.heads {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
border: 1px solid #e0e0e0;
}
.bodies {
padding: 4px;
border: 1px solid #f0f0f0;
}
....
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With