Can I have Component interfaces in React? - javascript

Say I have a <Modal> that takes a <Header> <Content> and <Footer>.
(
<Modal>
<Header>Foo</Header>
<Content>Foo</Content>
<Footer>Foo</Footer>
</Modal>
)
Now, inside my Modal component I'll probably have code like the following:
const header = children.find(child => child.type === Header)
In order to get a reference to the rendered header.
Now, what if from the consumer of the modal, I needed a decorated Header. Let's just call it DecoratedHeader
// DecoratedHeader
const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>
// consumer
(
<Modal>
<DecoratedHeader />
<Content>Foo</Content>
<Footer>Foo</Footer>
</Modal>
)
The line above wouldn't work anymore, as DecoratedHeader type is not Header. However, it IS rendering a Header.
It feels like there's the concept of "interface" which is missing. Ultimately, the Modal cares for a Header to be rendered, but if you wrap it under a "custom" component there's no way for it to know that it is still a Header.
What am I missing?
EDIT
To expand more about my use cases, I don't need an alternative solution. I need to know whether React has support for a mechanism equivalent to an interface, where 2 different Components that comply with the Liskov Substitution Principle (meaning they're swappable) can have a way to be picked by the parent.
Specifically, replacing this "hardcoded implementation" search, with an "interface" search:
-const specificChild = children.find(child => child.type === SomeComponent)
+const componentInterface = children.find(child => ????)
// Get a prop out of that component interface
const { someInterfaceProp } = componentInterface.props;
return (
<div>
{componentInterface} {/* render it on a specific place */}
</div>
)

Assuming the only thing you're going to be doing with these components is rendering them in specific spots of the modal, i would do them as separate props. For example:
const Modal = ({ header, content, footer }) => {
return (
<div>
{header}
<SomethingElseAllModalsHave />
{content}
{footer}
</div>
)
}
// ... used like:
const Example = () => {
return (
<Modal
header={<DecoratedHeader />}
content={<Content>Foo</Content>}
footer={<Footer>Foo</Footer>}
/>
)
}
If you need the modal to not just render the other components, but give them some information too, you could use a render prop. Basically the same as my example above, but now you pass in functions instead of elements
const Modal = ({ header, content, footer }) => {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
{header(isVisible)}
<SomethingElseAllModalsHave />
{content(isVisible)}
{footer(isVisible}
</div>
)
}
// ... used like:
const Example = () => {
return (
<Modal
header={() => <DecoratedHeader />}
content={(isVisible) => <Content>{isVisible ? "Foo" : "Bar"</Content>}
footer={(isVisible) => isVisible ? <Footer>Foo</Footer> : null}
/>
)
}
EDIT:
When you write the JSX <DecoratedHeader/>, the object that is produced contains no information about <Header>. It's basically just an object with a type (ie, a reference to DecoratedHeader) and some props (none in this case). Header only enters the picture when DecoratedHeader is rendered, which won't be until after Modal is rendered.
So whatever the characteristics are that Modal will use to identify what is and is not a header, it needs to be something that is on DecoratedHeader, not just on Header. Perhaps you could add a static property to any component that counts as a header, and then check for that:
const Header = () => {
// Whatever the code is for this component.
}
Header.isHeader = true;
const DecoratedHeader = () => <Header>Foo <Icon type="lock" /></Header>
DecoratedHeader.isHeader = true;
Then you'll look for it something like this (you should use React.Children, because children is not guaranteed to be an array):
const header = React.Children.toArray(children).find(child => child.type.isHeader);

Related

Can I store a component in a variable and push children to it after it has been created?

Let's say that I have this component:
const Test = ({ children, ...rest }) => {
return <>{children}</>
};
export default Test;
I am wondering if it is possible to create a variable that holds the component like this:
const test = <Test></Test>;
And then loop over some data and push children to the test variable on every iteration.
if you don't have the data yet, then all you have to do is conditionally render your component when you do have the data.
{ data ? (<Test>{data.map(...)}</Test>) : <SomeOtherComponent /> /* or null */}
or
{ data ? <>{data.map((x) => <Test>{x}</Test>)}</> : <SomeOtherComponent /> /* or null */}
depending on what you want achieve, i didn't fully understand your question
i.e. if you have the data you need, render the component, rendering the children as you see fit, otherwise render some other component (or null, to render nothing)
Yeap, try that pattern:
const test = (children) => <Test>{children}</Test>;
and usage
<>
{[1,2,3].map(el=>test(el))}
</>
[Edited]
const TestComp = ({children}) => <Test>{children}</Test>;
<>
{[1,2,3].map(el=>(<TestComp>{el}</TestComp>))}
</>

Way to render a new component onClick in react js

Am trying to render a new component onclick a button in react js. Am using functional components and I can't handle it. Eg: am in the UserManagement component and on a button click I need to render another component named employee management.
You can conditionally render your component.
Example :
EmployeeManagement.js
const EmployeeManagement = () => {
....
return (
<div>
EmployeeManagement
</div>
);
}
UserManagement.js
const UserManagement = () => {
const [hasRender, setRender] = useState(false);
const onShow = React.useCallback(() => setRender(true), []);
return (
<>
<button onClick={onShow}>Show Employee Management</button>
{hasRender && <EmployeeManagement />}
</>
)
}
One way to do this would be to add a local state in UserManagement,
that holds a boolean value indication whether the component should be hidden or shown.
Then you will have something like:
function UserManagement() {
const [compIsShown, setCompIsShown] = useState(false);
return (
// Whatever else you're rendering.
<button onClick={() => setCompIsShown(true)}>...</button>
{compIsShown && <OtherComp />}
)
}
What will happen is that compIsShown will initialize as false,
so this condition compIsShown && <OtherComp /> will prevent it from rendering.
Then, when you click the button, the state will set, causing a re-render, except now the condition will be true, so <OtherComp> will be rendered.
There are other ways to go about this.
Depends mostly on the use-case.
use a visible state & toggle it in onClick:
const [visible, setVisible] = useState(false)
onClick = () => {setVisible(true)}
then render it like this:
{visible && <EmployeeManagement onClick={onClick} />}

How to check if the element is present using react?

How can I check if an element is present in the DOM or not, using React?
I have a popup that is displayed throughout the application when items are 0 and the user clicks a button. It's created by a context provider that is wrapped around the App component.
There is an add button that gets displayed in some page "/items".
const root = () => {
<PopupContextProvider>
<App/>
</PopupContextProvider>
}
export const PopupContextProvider = ({ children }: any) => {
return (
<popupContext.Provider value={context}>
{children}
{(condition1 || condition2) && (
<Popup onHide={dismiss} />
)}
</popupContext.Provider>
);
}
function App() {
return (
<Route path="/items">
<Drawer/>
/>
//other routes
);
}
function Drawer() {
return (
<ButtonElement/> //this is a styled div component and i want to check if this element is
//present in dom at the sametime when popup is there in dom
);
}
What I want to do?
I want to check if the ButtonElement is there in the DOM at the same time as the popup.
The ways that I have thought:
add an id to button element and check if it is present using document.getelementbyid (last option for me)
using ref, but I'm not sure how to do it
I want to use a ref to button element, but I don't know how to pass it to context.
What would be the best way to do this?
Use the useRef hook and pass it down to the child component. It'll be undefined
Assuming you're defining const popupContext = React.createContext(undefined); somewhere, roughly this should work:
const PopupContextProvider = ({ children }: any) => {
const popupRef = useRef(null);
return (
<popupContext.Provider value={popupRef}>
{children}
{(condition1 || condition2) && (
<Popup onHide={dismiss} ref={popupRef}/>
)}
</popupContext.Provider>
);
}
const Drawer = () => {
return (
<popupContext.Consumer>
{value => (value !== undefined)
? <ButtonElement popup={true}/>
: <ButtonElement/>
}
</popupContext.Consumer>
);
}
More info about context usage here: https://reactjs.org/docs/context.html#reactcreatecontext

How to pass props between two not-related and stateless component in React?

As title said, i'm try to find a way to pass my props from one component to anotehr taht are not related between each others. The only thing that related them is a link:
const ConversationPreview = (props) => {
var people = props.people;
const messages = props.messages;
const conversationName = people.map(person => {
// Filter message from conversation's sender
function filterMessageBySender(obj){
if ('from' in obj && obj.from === person.nickname) {
return true;
}
}
const messagesByCurrentSender = messages.filter(filterMessageBySender);
const lastMsg = messagesByCurrentSender[messagesByCurrentSender.length - 1]
function passProps() {
return <Conversation people={people} />
}
return (
<li key={person.id}>
<Link to={`/${person.nickname}`} onClick={passProps}>
<Avatar image={person.pic} />
{person.nickname}
<p> {lastMsg.body} </p>
</Link>
</li>
)
});
return (
<ul>
{conversationName}
{props.children}
</ul>
)
};
And this is the component that should receive the people props:
const Conversation = (props) => {
console.log(props);
//console.log(people);
function findConversationPerson(person){
return person.nickname === props.params.conversation
}
const currentPerson = peopleOld.find(findConversationPerson);
return (
<div>
<header>
<Avatar image={currentPerson.pic} />
<h1> {props.params.conversation} </h1>
</header>
<main>
<MessagesList sender={currentPerson}/>
</main>
</div>
)
};
Is there a(good) way or should I re-think the whole way i structured the app?
After lot of resource i discover this is quite a common problem and is mostly related to react-router.
The problem is that usually in react we pass props inside the component which have to be explicitly defined in the render func.
But sometimes we don't want to render them directly and maybe the router take care about when and where render them.
So how to pass props?
A way to do it is to clone the children with the passed data.
So, in your App render function you will have:
{React.cloneElement(this.props.children, {dataChildrenNeeded: this.props.parentsData})}
So all your children will have same props.
And they can easily be access by doing:
this.props.dataChildrenNeeded
This will work with state or whateverlse you want to pass.
For better explanation and further reading here are my main sources:
Stackoverflow: React router and this.props.children
react-router github issue #1857

React Transferring Props except one

React suggests to Transfer Props. Neat!
How can I transfer all of the props but one?
render: function(){
return (<Cpnt {...this.propsButOne}><Subcpnt one={this.props.one} /></Cpnt>);
}
You can use the following technique to consume some of the props and pass on the rest:
render() {
var {one, ...other} = this.props;
return (
<Cpnt {...other}>
<Subcpnt one={one} />
</Cpnt>
);
}
Source
What you need to do is to create a copy of the props object and delete the key(s) you don't want.
The easiest would be to use omit from lodash but you could also write a bit of code for this (create a new object that has all the keys of props except for one).
With omit (a few options at the top, depending on what package you import/ES flavor you use):
const omit = require('lodash.omit');
//const omit = require('lodash/omit');
//import { omit } from 'lodash';
...
render() {
const newProps = omit(this.props, 'one');
return <Cpnt {...newProps}><Subcpnt one={this.props.one} /></Cpnt>;
}
If you have a lot of props you don't want in ...rest e.g. defaultProps, it can be annoying to write all of them twice. Instead you can create it yourself with a simple loop over the current props like that:
let rest = {};
Object.keys(this.props).forEach((key, index) => {
if(!(key in MyComponent.defaultProps))
rest[key] = this.props[key];
});
Thank you #villeaka!
Here's an example of how I used your solution for other people to better understand it's usage.
I basically used it to create a stateless wrapping-component that I then needed to pass its props to the inner component (Card).
I needed the wrapper because of the rendering logic inside another top level component that used this wrapper like this:
<TopLevelComponent>
{/* if condition render this: */}
<CardWrapper {...props}> {/* note: props here is TLC's props */}
<Card {..propsExceptChildren}>
{props.children}
</Card>
</CardWrapper>
{/* if other condition render this: */}
{/* ... */}
{/* and repeat */}
</TopLevelComponent>
where several conditions determine what comes after the H4 in the wrapper (see actual rendered node tree below).
So basically, I didn't want to duplicate code by writing the entire part that comes before {children} in the example below, for each arm of the conditional in the top level component that renders multiple variants of the wrapper from above example:
const CardWrapper: React.FC<IRecentRequestsCardProps> = (props) => {
const { children, ...otherProps } = props;
return (
<Card {...otherProps} interactive={false} elevation={Elevation.ONE}>
<H4>
Unanswered requests
</H4>
{children}
</Card>
);
};
And concrete usage in a React render function:
if (error)
return (
<CardWrapper {...props}>
<SimpleAlert title="Eroare" intent={Intent.DANGER}>
{error}
</SimpleAlert>
</CardWrapper>
);
if (loading)
return (
<CardWrapper {...props}>
<NonIdealState
icon="download"
title="Vă rog așteptați!"
description="Se încarcă cererile pentru articole..."
/>
</CardWrapper>
);
if (!data)
return (
<CardWrapper {...props}>
<NonIdealState
icon="warning-sign"
title="Felicitări!"
description="Nu există cereri fără răspuns."
/>
</CardWrapper>
);
// etc.
So the above just adds the H4 header before the children of the wrapper and also passes down the props that it has been passed down to, to the inner Card component.
The simplest way I found so far:
const obj = {
a: '1',
b: '2',
c: '3'
}
const _obj = {
...obj,
b: undefined
}
This will result in _obj having all the props except b
Try this:
function removeProps(obj, propsToRemove) {
let newObj = {};
Object.keys(obj).forEach(key => {
if (propsToRemove.indexOf(key) === -1)
newObj[key] = obj[key]
})
return newObj;
}
const obj = {nome: 'joao', tel: '123', cidade: 'goiania'}
const restObject = removeProps(obj, ['cidade', 'tel'])
console.log('restObject',restObject)
restObject
{
nome:"joao"
}
I had this issue when extending Material UI. A component would emit a warning if an unknown property was passed at all. I solved it slightly differently by specifically deleting the properties I didn't want to pass:
const passableProps = { ...props } as Partial<typeof props>;
delete passableProps.customValidity;
return (
<TextField { ...passableProps } // ...
);

Categories