Is it good idea to pass multiple components as a prop? - reactjs

I have a two menu components: profile-menu and main-menu.
Each menu has links with icons and component-button to open it.
Is it good idea to pass multiple components as links array and than render it with map like in this example? if not, what is alternative?
const Icon = () => <div>1</div>
const Icon2 = () => <div>2</div>
const links = [{ href: '/', name: 'MainPage', icon: Icon }, { href: '/test', name: 'Test', icon: Icon2 }]
const MainMenu = () => {
return (
<Navigation side='left' links={links}>
<img src='smt..' />
</Navigation>
)
}
const Profile = () => {
return (
<Navigation side='right' links={links}>
<img src='smt..' />
</Navigation>
)
}
const Navigation = ({ side, links, children }) => {
const menuRelayRef = useRef(null) // to close menu on page click
// some logic
return (
<Fragment>
{cloneElement(children, { ref: menuRelayRef })}
<div show={show} side={side}>
{links.map((l) => {
const Icon = l.icon // is it ok?
return (
<div>
<button>{l.name}</button>
<Icon />
</div>
)
})}
</div >
</Fragment>
)
}

i think it depends on your Icon component :
if Icon component is img tag so you should pass only src,alt.
if Icon component is SVG icon so you should pass SVG icon component.
generally if your component has same parts, so try to DRY (Dont Repeat Yourself),
and pass only dynamic parts of component.but sometimes you have no other way and passing component is best solution.

Related

Next.js - how to change the component that is displayed?

I'm brand new to React/Nextjs.
I'm using this as a template:
https://ui.mantine.dev/component/navbar-simple
This is the example data:
const data = [
{ link: '/notifications', label: 'Notifications', icon: IconBellRinging },
{ link: '/billing', label: 'Billing', icon: IconReceipt2 },
{ link: '/security', label: 'Security', icon: IconFingerprint },
It is used to built a navbar:
export function NavbarSimple() {
const { classes, cx } = useStyles();
const [active, setActive] = useState('Billing');
const links = data.map((item) => (
<a
className={cx(classes.link, { [classes.linkActive]: item.label === active })}
href={item.link}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span>{item.label}</span>
</a>
));
return (
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{/*
I am trying to get the components to be swapped/updated here
*/}
</AppShell>
Goal: If someone clicks "Security" in the navbar, the Security component will load.
Let's say I have built the "Notifications", "Billing" and "Security" components.
To update DOM, I saw a guide for using react-router-dom to do this. But I am trying to stick with only Nextjs.
Whatever string is stored in "link" can be changed. But from the "link" in the data object, is there a way to update the component?
If someone can point me to a tutorial, example, or even what to search for, I'd greatly greatly appreciate it :) I've been researching this evening but have not found anything yet.
I also made a codesandbox: https://wytec9.csb.app/
You could have within this component a function that will bring the component you want depending on the value, a quick example for this:
const renderComponent = () => {
if(active === 'Billing'){
return <Billing/>
} else if (){
// you get the idea
}
}
Now call that function to bring up the right component:
return (
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{renderComponent()}
</AppShell>
You could achieve this by modifying your current data array of objects, and adding a component corresponding to each link.
Here is what it would looks like
const data = [
{ link: '/notifications', label: 'Notifications', icon: IconBellRinging, component: <Notification /> },
{ link: '/billing', label: 'Billing', icon: IconReceipt2, component: <Billing /> },
{ link: '/security', label: 'Security', icon: IconFingerprint, component: <Security /> }
]
Create a state that will store the component (you could modify your active state to be an object containing both the label and object):
const [activeComponent, setActiveComponent] = useState(null);
Then, update it in your onClick.
<a
className={cx(classes.link, { [classes.linkActive]: item.label === active })}
href={item.link}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
setActiveComponent(item.component)
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span>{item.label}</span>
</a>
Good, you then can render the active component where you need it:
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{activeComponent}
</AppShell>

React: How to prevent menu component from being remounted when switching pages

Let's say that we have a React app with two pages A and B using a shared menu component Menu.
Our app renders either page A or page B, like the example below:
const Menu = (props) => {
React.useEffect(()=>{
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick('a')}>A</button>
<button onClick={() => props.onClick('b')}>B</button>
</div>
);
}
const PageA = (props) => {
const .. = useSomeHooksUsedByPageA();
return (
<div>
<Menu {...somePropsFromPageA} />
<div>Content of page A</div>
</div>
);
}
const PageB = (props) => (
const .. = useSomeHooksUsedByPageB();
<div>
<Menu {...somePropsFromPageB} />
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
switch (pageKey)
{
case "a":
return <PageA key="1" onClick={setPageKey} />;
case "b":
return <PageB key="1" onClick={setPageKey} />;
}
return "true"
}
Now, every time we switch pages (from A to B, or B to A), the menu is remounted and a message is printed to the console.
Using this component hierarchy where the menu receives props from the page, is there any way to tell React not to remount the menu when we switch pages?
(A typical use-case could be that the menu has a scroll, and we want to keep the scroll position when navigating different pages.)
Help is greatly appreciated!
One potential solution for this problem is to move <Menu/> into the <App/> component, and render each page after the menu.
This provides a couple of benefits:
The Menu won't be re-rendered whenever the page changes.
The onClick function does not need to be passed through props on each page just to provide it to the <Menu/> component nested within.
const Menu = (props) => {
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button onClick={() => props.onClick("a")}>A</button>
<button onClick={() => props.onClick("b")}>B</button>
</div>
);
};
const PageA = () => (
<div>
<div>Content of page A</div>
</div>
);
const PageB = () => (
<div>
<div>Content of page B</div>
</div>
);
const App = () => {
const [pageKey, setPageKey] = React.useState("a");
let page;
switch (pageKey) {
case "b":
page = <PageB key="2" />;
break;
default:
page = <PageA key="3" />;
break;
}
return (
<>
<Menu onClick={setPageKey} />
{page}
</>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
Edit
Further to #glingt's comment regarding the hierarchy and how this needs to function, Context might be a good candidate for the use case. If pages need to update the <Menu/> component's props, then using context to manage state between the menu and pages might be a better solution in terms of architecture. Instead of rendering many <Menu/> components inside of each child, only one <Menu/> can be rendered higher up in the tree. This results in the component mounting once rather than many times with each child. Effectively, context manages the state of the menu, and provides methods to update state to any children under the provider. In this case, both child pages and the menu can update and respond to state updates.
import "./styles.css";
import React, { useContext, useMemo, useState } from "react";
// Create an instance of context so we are able to update the menu from lower in the tree
const menuContext = React.createContext({});
// Add state to the context provider. Wrap props earlier in the tree with this component.
const MenuContext = ({ children }) => {
const [pageKey, setPageKey] = useState("a");
const value = useMemo(() => ({ pageKey, setPageKey }), [pageKey]);
return <menuContext.Provider value={value}>{children}</menuContext.Provider>;
};
// The menu component which will:
// 1. Update the menuContext when the user selects a new pageKey
// 2. Respond to updates made to the pageKey by other components (in this case pages)
const Menu = () => {
const { pageKey, setPageKey } = useContext(menuContext);
React.useEffect(() => {
console.log("The menu remounted");
}, []);
return (
<div id="menu" className="has-scrollbar">
<button
onClick={() => setPageKey("a")}
style={{ color: pageKey === "a" ? "blue" : "red" }}
>
A
</button>
<button
onClick={() => setPageKey("b")}
style={{ color: pageKey === "b" ? "blue" : "red" }}
>
B
</button>
</div>
);
};
// In each page, we are able to update a value that is consumed by the menu using setPageKey
const PageA = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page A</div>
<button onClick={() => setPageKey("b")}>Go to page B</button>
</div>
);
};
const PageB = () => {
const { setPageKey } = useContext(menuContext);
return (
<div>
<div>Content of page B</div>
<button onClick={() => setPageKey("a")}>Go to page A</button>
</div>
);
};
const PageComponent = () => {
const { pageKey } = useContext(menuContext);
switch (pageKey) {
case "b":
return <PageB key="2" />;
default:
return <PageA key="1" />;
}
};
const App = () => (
<MenuContext>
<Menu />
<PageComponent />
</MenuContext>
);
ReactDOM.render(<App />, document.querySelector("#app"))

how to add components into reusuable component? react

I'm not sure my title make sense :/ sorry for my poor description.
anyway what I was trying was.. making a reusuable component.
and the props of the reusuable component is another component.
here's what I did:
const Accordion = ({ title, content }) => {
return (
<Wrapper>
<Title>{title}</Title>
<Content>{content}</Content>
</Wrapper>
);
};
const ParentComponent = () => {
const title = () => <div>title</div>;
const content = () => {
return (<div><h2>...text...</h2><div>...text..</div></div>)
}
return <Accordion title={title} content={content} />;
};
it seems nice to me, but it does not work 🤯
the title and the content(...text...) was not showing at all.
it works in this way though, this is not what I want 🤯🤯
<Accordion title='title text' content='content context' />;
thanks for your help.
Just do this:
You need to render it like <element />
const Accordion = ({ title, content }) => {
return (
<Wrapper>
<Title><title /></Title>
<Content><content /></Content>
</Wrapper>
);
};

React onClick to affect only this iteration

I have a page that uses an object that contains lists within lists. I have all the components showing the data correctly, but I'm trying to add a toggle button for each primary list item so you can show/hide their child lists. I had previously made something that would affect EVERY instance of the component when clicked, so when you click the expand button it would toggle the child lists of EVERY primary item.
React is new to me and I'm using this project partially as a learning tool. I believe this has to do with binding state to the specific instance of the component, but I'm not sure how or where to do this.
Here is the component:
const SummaryItem = props => {
const summary = props.object;
return(
<div className="summary_item">
{Object.entries(summary).map( item =>
<div>
Source: {item[0]} <br />
Count: {item[1].count} <br />
<button onClick={/*expand only this SummaryItemList component*/}>expand</button>
<SummaryItemList list={item[1].items} />
</div>)
}
</div>
);
}
I previously had a state hook that looked like:
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => setIsExpanded(!isExpanded);
And in my render function the button had the toggle function in the onClick:
<button onClick={toggle}>expand</button> and I had a conditional if(isExpanded) with two renders, one with the SummaryItemList component and one without.
Is there a better way to do this besides mapping the object, and how do I bind the state of the toggle to affect only the instance it's supposed to affect?
I think you maybe forgot to give each item an isExpanded, the best way to do this is to split up your items and item in different components (in the example below it List for items and Item for item).
const { useState } = React;
const Item = ({ name, items }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggle = () => {
setIsExpanded((s) => !s);
};
return (
<li>
{name}
{items && (
<React.Fragment>
<button onClick={toggle}>
{isExpanded ? '-' : '+'}
</button>
{isExpanded && <List data={items} />}
</React.Fragment>
)}
</li>
);
};
const List = ({ data }) => {
return !data ? null : (
<ul>
{Object.entries(data).map(([key, { items }]) => (
<Item key={key} items={items} name={key} />
))}
</ul>
);
};
const App = () => {
const data = {
A: {
items: {
AA1: { items: { AAA1: {}, AAA2: {} } },
AA2: { items: { AAA: {} } },
},
},
};
return <List data={data} />;
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

PropTypes for specific children components

I’m struggling to figure out how to specify PropTypes for a set of specific children components. My dialog component is allowed to get components of the type Title, Body and/or Footer. All these components may only be used once but can appear together at the same time.
Is there a recommended way to specify an appropriate PropType?
const Title = ({ text }) => (
<h1>{ text }</h1>
);
const Body = ({ text }) => (
<p>{ text }</p>
);
const Footer = ({ text }) => (
<small>{ text }</small>
);
const Dialog = ({ children }) => (
React.Children.map(children, (child) => {
return (
<div>{child}</div>
)
})
);
Dialog.propTypes = {
children: PropTypes.oneOfType([
PropTypes.instanceOf(Title),
PropTypes.instanceOf(Body),
PropTypes.instanceOf(Footer)
]).isRequired
}
Edited
You could use a custom PropType validation to check if the prop value fits the rules your component expects: component name (Title, Body and/or Footer), if there is only one of each or not, the order of these components...
But it's overkill.
The best in my opinion it's to have these components inside the Dialog component and use props to customize it. eg:
CodeSandbox
const Title = ({ children }) => <h2>{children}</h2>;
const Dialog = ({
title,
showConfirmationButton,
showCancelButton,
onConfirmation,
onCancel,
children
}) => (
<div>
{!!title && <Title>{title}</Title>}
{children}
{(showConfirmationButton || showCancelButton) && <hr />}
{showCancelButton && <button onClick={onCancel}>Cancel</button>}
{showConfirmationButton && <button onClick={onConfirmation}>Ok</button>}
</div>
);
Dialog.propTypes = {
title: PropTypes.string,
showOkButton: PropTypes.bool,
onClickOk: PropTypes.func,
children: PropTypes.any.isRequired
};
export default function App() {
return (
<Dialog
title="Title dialog"
showConfirmationButton
onConfirmation={() => console.log("ok")}
showCancelButton
onCancel={() => console.log("cancel")}
>
<p>The Dialog body as children.</p>
</Dialog>
);
}
Or if you still need to pass the component as a prop, then the best should be:
const Dialog = ({ title, body, footer }) => (
<div>
{ title }
{ body }
{ footer }
</div>
)
Dialog.propTypes = {
title: PropTypes.instanceOf(Title),
footer: PropTypes.instanceOf(Footer),
body: PropTypes.instanceOf(Body)
}
You can check the available prop types here https://reactjs.org/docs/typechecking-with-proptypes.html

Resources