React composition - How to call parent method - javascript

I'm using composition in React and would like to call a parent method. All of the examples I've found use inheritance.
Container component - Inserts a child component
interface ContainerProps {
children: ReactNode;
}
function Container(props: ContainerProps) {
const [showApply, setShowApply] = useState<boolean>(false);
return (
<>
<div>Children</div>
{props.children}
</>
);
// I want to call this method from the `children`
function calledByChild(){}
}
Composition - Needs to call Container method when button is clicked
function CombinedComponent() {
return <Container handleApplyClicked={handleApplyClicked}>
<Button type="primary" shape="round" onClick={tellContainerThatButtonWasClicked}>
</Container >
}
When the button is clicked in the CombinedComponent I would like it to inform the Container. The examples I've seen use inheritance and pass the parents method to the child but in this case the child is defining the parent within it.
How can this be achieved?
Update
I've tried adding this to the parent but the child components don't seem to have the extra property added.
{React.cloneElement(props.children as React.ReactElement<any>, { onClick: myFunc })}
Child interface/props
interface CombinedComponentProps{
// This value is always undefined
onClick?: () => void;
}
function CombinedComponent(props: CombinedComponentProps) {
...
// Undefined
console.log(props.onClick)
}

I recently had to do something similar and, inspired by this post, I ended up with:
const WrapperComponent = ({ children }) => {
const myFunc = React.useCallback(() => {
// ...
}, []);
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { onClick: myFunc });
}
});
}
[edit] a working demo:
The following snippet demonstrate how the above approach could be used to read a child prop from the wrapper/parent component.
Please be aware that it might take a few seconds for the snippet to load and run; I did not investigate the reason for this, as it is out of the scope for the question.
const App = () => {
return (
<div>
<h1>MyApp</h1>
<WrapperComponent>
<button id='btn1'>btn1</button>
<button id='btn2'>btn2</button>
<button id='btn3'>btn3</button>
<div className='fakeBtn' id='div1'>div1</div>
</WrapperComponent>
</div>
);
}
const WrapperComponent = ({ children }) => {
const [clickedChildId, setClickedChildId] = React.useState();
const myFunc = React.useCallback((id) => {
setClickedChildId(id)
}, [setClickedChildId]);
React.useEffect(() => {
clickedChildId && console.log(`the clicked child ID is ${clickedChildId}`);
}, [clickedChildId]);
return React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { onClick: () => myFunc(child.props.id) });
}
});
}
ReactDOM.render(<App />, document.querySelector('#mountNode'))
div.fakeBtn {
background-color: #cdf;
padding: 5px;
margin: 3px 0;
border: 1px solid #ccc;
border-radius: 2px;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id='mountNode'></div>

You can do it by cloning the children, and giving it the props that you want:
React.cloneElement(children, {calledByChild})
This way, you add the function calledByChild to the children, so you can call it from the children component.
It could look like this:
const Parent = ({ children }) => {
const func = () => console.log("click in Parent");
return (
<>
<div>children</div>
{cloneElement(children, {func})}
</>
);
};
const Children = ({func}) => {
return <button onClick={func}>Click</button>;
};
Take a look at this article

Related

React: Pass 2 props back to the parent component in the same function from the child

I want to set the state and close the modal at the same time, both are done by passing props back to the parent component. The problem I'm having is that only 1 seems to want to work at a time. I can get both working by themselves, but as soon as they're both there it doesn't work. How can I solve this?
CHILD COMPONENT
useSelectedImage = () => {
this.props.saveChosenImage(this.state.imageChosen)
this.props.closeModal();
};
<button onClick={this.useSelectedImage}>INSERT IMAGE</button>
PARENT COMPONENT
state = {
imageModalOpen: false,
}
// open the image modal
openImageModal = () => {
this.setState({ ...this.state, imageModalOpen: true })
};
// close the image modal
handleImageClose = () => {
this.setState({ ...this.state, imageModalOpen: false })
};
<Modal
open={this.state.imageModalOpen}
onClose={this.handleImageClose}
>
<DialogContent className={classes.DialogContent}>
<SelectImageModal saveChosenImage={this.saveChosenImage} closeModal={this.handleImageClose}/>
</DialogContent>
<modal>
saveChosenImage = (image) => {
this.setState({ ...this.state, imageChosen: image })
};
try like this.
CHILD COMPONENT
useSelectedImage = () => {
this.props.saveChosenImage(this.state.imageChosen);
- this.props.closeModal(); //remove this
};
...
PARENT COMPONENT
...
saveChosenImage = (image) => {
this.setState({ ...this.state, imageChosen: image, imageModalOpen: false }); // modified here
};
You could call one function inside the other. This way you maintain the purity of your methods. You could still reuse handleImageClose on a close button without selecting an image.
See the example on codesandbox https://codesandbox.io/s/sweet-torvalds-3dyv5
import React, { Component } from "react";
import ReactDOM from "react-dom";
const Child = props => {
const useSelectedImage = () => {
console.log("clicked");
// DO THIS
return props.closeModal(props.saveChosenImage(props.theImage));
};
return (
<div>
<button onClick={useSelectedImage}>Select Image</button>
<button onClick={props.closeModal}>Cancel</button>
</div>
);
};
class Parent extends Component {
openImageModal = () => console.log("OPEN FIRED");
handleImageClose = () => {
console.log("CLOSED FIRED");
this.setState({ closed: true });
};
saveChosenImage = image => {
console.log(`SAVED!! ${image}`);
this.setState({ image });
};
render() {
console.log(this.state);
return (
<Child
saveChosenImage={this.saveChosenImage}
closeModal={this.handleImageClose}
theImage="cow.jpg"
/>
);
}
}
function App() {
return (
<div className="App">
<Parent />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

How to pass callback functions to parent

Suppose we have a Container component as follows.
class Container extends React.Component {
handleClose = () => {
// need to ask 'Content' is it okay with closing
const isContentAgree = /* */;
if (isContentAgree) this.props.onClose();
};
render () {
const {content: Content} = this.props;
return (
<button onClick={this.handleClick}>Close</button>
<Content {/* some container-specific props */} />
);
}
}
Usage:
<Container content={SomeComponent}/>
In this scenario how can I pass a callback function from SomeComponent to Container? This callback is to be called when the button in the Container is clicked and returns a boolean value.
You need to keep isContentAgree in state and you can pass function to toggle isContentAgree to child component.
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
isContentAgree: false
}
}
toggleContentAgree = (e) => {
this.setState({ isContentAgree: e.target.value })
}
handleClose = () => {
// need to ask 'Content' is it okay with closing
const isContentAgree = this.state.isContentAgree;
if (isContentAgree) this.props.onClose();
};
render () {
const {content: Content} = this.props;
return (
<button onClick={this.handleClose}>Close</button>
<Content toggleContentAgree={this.toggleContentAgree} />
);
}
}
you can simply pass the callback function as prop to the component.
<Content onHide={handleClose} />
in Component you have to call props.onHide function as needed.
You can use:
React.cloneElement(SomeComponent, [props...])
And as "props" to pass a function that updates container state.
You should do it using store (Redux/Mobx/ContextAPI). That's the ideal way to do it.
But...
You can still pass the callback function:
class Container extends React.Component {
render () {
const {content: Content, callback} = this.props;
return (
<button onClick={this.handleClick}>Close</button>
<Content onSomething={callback} {/* ... */} />
);
}
}
<Container content={SomeComponent} callback={someCallbackFunction}/>

React function component setTimeout - Multiple render calls and rerenders (recommended approach) (fires multiple times)

I have a Notification component that should close itself after a few seconds and call the onClose prop:
function Notification(props) {
console.log("Notification function component called");
setTimeout(() => {
props.onClose();
}, 4000);
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
In my App, I have a state that holds notifications object and I map through them.
class App extends React.Component {
constructor() {
super();
this.pushNotification = this.pushNotification.bind(this);
}
state = {
notifications: {}
};
pushNotification() {
const id = uuid();
const newNotifications = { ...this.state.notifications };
const date = new Date();
newNotifications[id] = {
id,
date: JSON.stringify(date)
};
this.setState({
notifications: newNotifications
});
}
removeNotification(id) {
console.log("removeNotification");
const newNotifications = { ...this.state.notifications };
delete newNotifications[id];
this.setState({
notifications: newNotifications
});
}
render() {
return (
<div className="App">
<button onClick={this.pushNotification}>Push notification</button>
{Object.keys(this.state.notifications).map(
(notificationIndexKey, index) => {
return (
<Notification
originalKey={JSON.stringify(index)}
key={notificationIndexKey}
onClose={() => {
console.log("Notfication fired on close");
this.removeNotification(notificationIndexKey);
}}
>
Notification{" "}
{this.state.notifications[notificationIndexKey].date}
</Notification>
);
}
)}
</div>
);
}
}
I've noticed that if I push multiple notifications in my state, the setTimout is initialized multiple times (which makes sense since render it's called every time the state is updated)
My question is, how would you recommend optimizing this so that the timeout to be invoked only once.
One method that I've tried is to create an array with items that I've removed and check before I call the prop.
Sandbox here: https://codesandbox.io/s/6y3my2y2jr
You should apply that side-effect when the component has mounted.
Currently your code will do this on render.
The render function can be called multiple times.
This code should reflect the correct changes.
class Notification extends React.Component {
componentDidMount() {
setTimeout(this.props.onClose, 4000);
}
render() {
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
}
You can do this by keeping a class property say notificationTimer initially set to null and can modify your Notification functions as:
function Notification(props) {
console.log("Notification function component called");
if (!this.notificationTimer)
this.notificationTimer = setTimeout(() => {
props.onClose();
}, 4000);
}
return (
<div>
{props.children}
<button onClick={props.onClose}>Close</button>
</div>
);
}
And in your close function you can do something like this:
onClose() {
// Your code.
if (this.notificationTimer) {
clearTimeout(this.notificationTimer);
this.notificationTimer = null;
}
}
This will not let you create multiple timers.

event.target is taking react child components separately

I have a menu box in my react app and I show when I click on show menu button and hiding it if menu its open. I want to hide menu if I click outside the menu
class MenuButton extends Component {
constructor (props) {
super (props)
this.state = {showMenu: false}
this.toggleMenu = this.toggleMenu.bind(this)
}
toggleMenu () {
let showMenu = !this.state.showMenu
this.setState({showMenu})
}
componentDidMount () {
window.addEventListner('click', e => {
if (e.target !== document.getElementById('menu-div') {
this.setState({showMenu: false})
}
})
}
render () {} {
return (
<div>
<button onClick={this.toggleMenu}>Menu</button>
{this.state.showMenu ? <div id='menu-div><Menu /></div> : null}
</div>
)
}
}
and my Menu Component has many child Components
const Menu = () => {
return (
<div>
<Component1/>
<Component2/>
<Component3/>
</div>
)
}
but clicking on these child components close my menu as event.target is giving different node
I would not use getElementById nor search elements in the DOM. This is not the "React Way" of doing things and it's considered a bad practice.
Instead use the refs API that react provides and grab a reference to the node.
You can add event listener of mousedown and check if the ref contains the target, if not then it means you are outside the ref (the menu in your case). So all is left is to set the state to close it.
Here is a running snippet of your code with the implementation i mentioned:
class MenuButton extends React.Component {
constructor(props) {
super(props);
this.state = { showMenu: false };
}
componentDidMount() {
document.addEventListener("mousedown", this.handleOutsideClick);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleOutsideClick);
}
toggleMenu = () => {
let showMenu = !this.state.showMenu;
this.setState({ showMenu });
};
handleOutsideClick = event => {
if (this.menuRef && !this.menuRef.contains(event.target)) {
this.setState({ showMenu: false });
}
};
setRef = ref => {
this.menuRef = ref;
};
render() {
const { showMenu } = this.state;
return (
<div>
<button onClick={this.toggleMenu}>Menu</button>
{<Menu className={`${!showMenu && "hide"}`} setRef={this.setRef} />}
</div>
);
}
}
class Menu extends React.Component {
render() {
const { setRef, className } = this.props;
return (
<div className={`menu ${className}`} ref={setRef}>
<div>comp 1</div>
<div>comp 2</div>
<div>comp 3</div>
</div>
);
}
}
ReactDOM.render(<MenuButton />, document.getElementById("root"));
.hide{
display: none;
}
.menu{
border: 1px solid #333;
padding: 5px;
max-width: 60px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Dynamically wrapping a random number React components at run time

If I have say the following simple components:
const CompOne = (props) => {
return (
<div className="compOne">
{props.children}
</div>
);
};
const CompTwo = (props) => {
return (
<div className="compTwo">
{props.children}
</div>
);
};
const CompThree = (props) => {
return (
<div className="compThree">
{props.content}
</div>
);
};
Now during run time, after making an AJAX request the client receives information that gives the order in which components need to wrap into one another. The result of that AJAX request would look something like this:
let renderMap = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: { content: "hi there" } }
];
So the composition should flow by iterating through the array and composing one component into the next. e.g: CompOne(CompTwo(CompThree)))
Two important things to note when I tried creating a wrapping HOC to fix this issue:
Edit: Important detail I forgot to mention in the original post
1) The number of components to wrap will not be consistent. At times it could be 3, but other times as many as 4, 5, 6 components needed to wrap into each other
2) The order could be different each time
<CompOne>
<CompTwo>
<CompThree content="hi there">
</CompThree>
</CompTwo>
</CompOne>
So my resulting HTML would be:
<div className="compOne">
<div className="compTwo">
<div className="compThree">
hi there
</div>
</div>
</div>
I've tried various things but I can't get it to work once I start getting past just wrapping two components. Is this something I can even do in React?
Like the link that Arup Rakshit posted in the comments showed, you can use components that are stored in a variable - with JSX - as long as they are capitalized:
// in render()
const MyComp = props.someVariableContainingAComponent;
return <MyComp />;
With that in mind, one approach to your problem would be to iterate through all your components, starting with inner one, and then taking the each of the next to use as a wrapper for the previous one. Given the shape of your test data renderMap, and using Array.protype.reduce for the iteration, it could look something like this:
renderComponents(renderMap) {
const Component = renderMap
.reverse()
.reduce( (ComponentSoFar, {component, props}) => {
const Outer = component;
return () => (<Outer {...props} ><ComponentSoFar /></Outer>);
}, props => null ); // initial value, just a "blank" component
return ( <Component /> );
}
I have included a demo showing how both different number of components and varying order of nesting can be handled with this approach.
const CompOne = (props) => (
<div className="comp compOne"><p>One:</p>{ props.content || props.children } </div>);
const CompTwo = (props) => (
<div className="comp compTwo"><p>Two:</p> { props.content || props.children }</div>);
const CompThree = (props) => (
<div className="comp compThree"><p>Three:</p> { props.content || props.children }</div>);
const CompFour = (props) => (
<div className="comp compFour"><p>Four:</p> { props.content || props.children }</div>);
const CompFive = (props) => (
<div className="comp compFive"><p>Five:</p> { props.content || props.children }</div>);
const renderMap1 = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: {} },
{ component: CompFour, props: {} },
{ component: CompFive, props: { content: "hi there" } }
];
const renderMap2 = [].concat(renderMap1.slice(1,4).reverse(), renderMap1.slice(4))
const renderMap3 = renderMap2.slice(1);
class App extends React.Component {
constructor(props) {
super(props);
}
renderComponents(renderMap) {
const Component = renderMap
.reverse()
.reduce( (ComponentSoFar, {component, props}) => {
const Outer = component;
return () => (<Outer {...props} ><ComponentSoFar /></Outer>);
}, props => null ); // initial value, just a "blank" component
return ( <Component /> );
}
render() {
return ( <div>
{ this.renderComponents(renderMap1) }
{ this.renderComponents(renderMap2) }
{ this.renderComponents(renderMap3) }
</div> );
}
};
ReactDOM.render(<App />, document.getElementById('root'));
.comp {
border: 5px solid green;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compOne { border-color: red;}
.compTwo { border-color: green;}
.compThree { border-color: blue;}
.compFour { border-color: black;}
.compFive { border-color: teal;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit: New info was added to the question. Given that info, this approach doesn't work.
You can probably solve it using a Higher-order component (HOC), something like this:
const superWrapped = (Outer) => (Middle) => (Inner) => props => {
return (
<Outer>
<Middle>
<Inner content={props.content} />
</Middle>
</Outer>
)
};
Where you would later use it like this:
render() {
const SuperWrapped =
superWrapped(CompOne)(CompThree)(CompTwo); // any order is fine!
return (<SuperWrapped content="Something here.." /> );
}
Some minor adjustments on your components would be necessary for this to work. I've included a working demo below:
const superWrapped = (Outer) => (Middle) => (Inner) => props => {
return (
<Outer>
<Middle>
<Inner content={props.content} />
</Middle>
</Outer>)
};
const CompOne = (props) => {
return (
<div className="compOne">
<p>One:</p>
{props.children || props.content}
</div>
);
};
const CompTwo = (props) => {
return (
<div className="compTwo">
<p>Two:</p>
{props.children || props.content}
</div>
);
};
const CompThree = (props) => {
return (
<div className="compThree">
<p>Three:</p>
{props.children || props.content}
</div>
);
};
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
const components = getComponentOrder();
const SuperWrapped1 =
superWrapped(components[0])(components[1])(components[2]);
const SuperWrapped2 =
superWrapped(components[2])(components[1])(components[0]);
return (
<div>
<SuperWrapped1 content="Hello, world!" />
<SuperWrapped2 content="Goodbye, world!" />
</div>
);
}
}
const getComponentOrder = () => {
return Math.random() < 0.5 ?
[CompOne, CompTwo, CompThree] :
Math.random() < 0.5 ?
[CompThree, CompOne, CompTwo] :
[CompTwo, CompOne, CompThree]
};
ReactDOM.render(<App />, document.getElementById('root'));
.compOne {
border: 5px solid red;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compTwo {
border: 5px solid green;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compThree {
border: 5px solid blue;
padding: 5px;
margin: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
The easiest way to achieve this is to use a recursive function:
let renderMap = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: { content: "hi there" } }
];
function App() {
let index = 0;
let structure = "Hi There!"
function packing() {
if (index === renderMap?.length) return;
const Comp = renderMap[index].component
structure = <Comp>{structure}</Comp>;
index += 1;
packing();
}
packing();
return <>{structure}</>
}

Categories

Resources