Triggering a function in a React parent functional component from its child - javascript

Seen similar issues here, but couldn't wrap my mind on how this works. New to functional components and React overall.
Parent contains the Child, which is a modal. Parent has a div that triggers showing the Child modal, and the Child modal has a close button that triggers its hiding. When I click on the div component in Parent, I need to show and hide the Child modal. When I click on the close button in the Child, I need to hide the Child component.
The Parent component:
import React, { useState } from "react";
import Child from "./Child";
const Parent = () => {
const [buttonState, setbuttonState] = useState({
buttonState: false,
});
const onParentClick = (e) => {
e.preventDefault();
setbuttonState(!buttonState);
};
return (
<div>
<div onClick={onParentClick}></div>
<Child isOpen={buttonState} onParentClick={onParentClick} />
</div>
);
};
export default Parent;
The Child component:
import React, { useState } from "react";
const Child = (props) => {
const [buttonState, setButtonState] = useState({
buttonState: props.isOpen,
});
const onChildClick = (e) => {
e.preventDefault();
setButtonState(false);
props.onParentClick();
};
return (
<div
className={
buttonState ? "child-modal-opened" : "child-modal-closed"
}
>
<div onClick={onChildClick}>Close</div>
</div>
);
};
export default Child;
For some reason, can't make this work. What am I missing here?

Looks like useState() is used incorrectly.
const [buttonState, setbuttonState] = useState({
buttonState: false,
});
results in buttonState being { buttonState: false}, so setbuttonState(!buttonState) does not work as intended.
Here's updated Parent component with useState(false) instead (setting initial buttonState value to false)
import React, { useState } from "react";
import Child from "./Child";
const Parent = () => {
const [buttonState, setbuttonState] = useState(false);
const onParentClick = (e) => {
e.preventDefault();
setbuttonState(!buttonState);
};
return (
<div>
<div onClick={onParentClick}></div>
<Child isOpen={buttonState} onParentClick={onParentClick} />
</div>
);
};
export default Parent;
P.S.
As #Will suggested, there is no need to create another state in Child, it can be passed from Parent
import React, { useState } from "react";
const Child = (props) => {
return (
<div
className={
props.isOpen ? "child-modal-opened" : "child-modal-closed"
}
>
<div onClick={props.onParentClick}>Close</div>
</div>
);
};

It looks like onParentClick is defined so as to take an event object as a parameter and call preventDefault() on that, but you're calling it without any arguments. Does it work like this: props.onParentClick(e);?

Related

React: How to setState in child component from parent component?

I'm new to React and am attempting to set up a Bootstrap modal to show alert messages.
In my parent App.js file I have an error handler that sends a Modal.js component a prop that triggers the modal to show, eg:
On App.js:
function App() {
const [modalShow, setModalShow] = useState(false);
// Some other handlers
const alertModalHandler = (modalMessage) => {
console.log(modalMessage);
setModalShow(true);
}
return (
// Other components.
<AlertModal modalOpen={modalShow}/>
)
}
And on Modal.js:
import React, { useState } from "react";
import Modal from "react-bootstrap/Modal";
import "bootstrap/dist/css/bootstrap.min.css";
const AlertModal = (props) => {
const [isOpen, setIsOpen] = useState(false);
if (props.modalOpen) {
setIsOpen(true);
}
return (
<Modal show={isOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
export default AlertModal;
However, this doesn't work. I get the error:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
If I change the Modal component to be a 'dumb' component and use the prop directly, eg:
const AlertModal = (props) => {
return (
<Modal show={props.modalOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
It does work, but I was wanting to change the show/hide state on the Modal.js component level as well, eg have something that handles modal close buttons in there.
I don't understand why is this breaking?
And does this mean I will have to handle the Modal close function at the parent App.js level?
Edit - full app.js contents
import React, { useState } from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.css';
import AddUserForm from './components/addUserForm';
import UserList from './components/userList';
import AlertModal from './components/modal';
function App() {
const [users, setUsers] = useState([]);
const [modalShow, setModalShow] = useState(false);
const addPersonHandler = (nameValue, ageValue) => {
console.log(nameValue, ageValue);
setUsers(prevUsers => {
const updatedUsers = [...prevUsers];
updatedUsers.unshift({ name: nameValue, age: ageValue });
return updatedUsers;
});
};
const alertModalHandler = (modalMessage) => {
console.log(modalMessage);
setModalShow(true);
}
let content = (
<p style={{ textAlign: 'center' }}>No users found. Maybe add one?</p>
);
if (users.length > 0) {
content = (
<UserList items={users} />
);
}
return (
<>
<div className="container">
<div className="row">
<div className="col-md-6 offset-md-3">
<AddUserForm onAddPerson={addPersonHandler} fireAlertModal={alertModalHandler}/>
</div>
</div>
<div className="row">
<div className="col-md-6 offset-md-3">
{content}
</div>
</div>
</div>
<AlertModal modalOpen={modalShow}/>
</>
);
}
export default App;
In your modal.js
you should put
if (props.modalOpen) {
setIsOpen(true);
}
in a useEffect.
React.useEffect(() => {if (props.modalOpen) {
setIsOpen(true);
}}, [props.modalOpen])
You should never call setState just like that. If you do it will run on every render and trigger another render, because you changed the state. You should put the setModalShow together with the if clause in a useEffect. E.g.:
useState(() => {
if (modalOpen) {
setIsOpen(true);
}
}, [modalOpen])
Note that I also restructered modalOpen out of props. That way the useEffect will only run when modalOpen changes.
If you already send a state called modalShow to the AlertModal component there is no reason to use another state which does the same such as isOpen.
Whenever modalShow is changed, it causes a re-render of the AlertModal component since you changed it's state, then inside if the prop is true you set another state, causing another not needed re-render when you set isOpen. Then, on each re-render if props.showModal has not changed (and still is true) you trigger setIsOpen again and again.
If you want control over the modal open/close inside AlertModal I would do as follows:
<AlertModal modalOpen={modalShow} setModalOpen={setModalShow}/>
Pass the set function of the showModal state to the modal component, and there use it as you see fit. For example, in an onClick handler.
modal.js:
import React, { useState } from "react";
import Modal from "react-bootstrap/Modal";
import "bootstrap/dist/css/bootstrap.min.css";
const AlertModal = (props) => {
const onClickHandler = () => {
props.setModalOpen(prevState => !prevState)
}
return (
<Modal show={props.modalOpen}>
<Modal.Header closeButton>Hi</Modal.Header>
<Modal.Body>asdfasdf</Modal.Body>
</Modal>
);
};
export default AlertModal;

React how to focus div when contentEditable is set to true?

I've got some react code like the following (minimal reproducible example):
import React, { useState } from 'react';
const Todo = ({todo}) => {
const [isUpdating, setisUpdating] = useState(false)
const updateItemHandler = (e) => {
setisUpdating(true);
}
return(
<div onClick={updateItemHandler} className={fa ${isUpdating ? "fa-check" : "fa-pencil"}`}></div>
<div id={todo.id}>
<div suppressContentEditableWarning={true}
contentEditable = {isUpdating}>{todo.value}
</div>
</div>
)
}
export default Todo;
When I clicked the div, it does change contentEditable to true, but I would also like to focus the newly editable div at the same time. I tried modifying my updateItemHandler function like so:
const updateItemHandler = (e) => {
setisUpdating(true);
e.focus();
}
but React threw an error/said focus wasn't a function here.
Is there some way I can automatically focus the div when I change the contentEditable to true?
Thanks
You can try something like this
import React, { useState, useRef } from 'react';
const Todo = ({todo}) => {
const ref = useRef(null)
const [isUpdating, setisUpdating] = useState(false)
const updateItemHandler = (e) => {
setisUpdating(true);
ref.current.focus()
}
return(
<div className="row text-center" id={todo.id}>
<div suppressContentEditableWarning={true}
contentEditable = {isUpdating} ref={ref}>{todo.value}
</div>
</div>
)
}
export default Todo;
Using ref (reference) may help you

Moving logic to child component from a parent

I am in situation where a function located in parent component handles onOK for Modal Component, the reason i have located it in parent is due to another local function that gets called once onOk button gets clicked
I would like to move it to child since Modal should be responsible for onOK logic , i can make two components hold visible state or create a stores but the question is how to go about it if another function is involved and that function is glued in parent
Parent
handleModalOk = () => {
this.onRadioButtonChange(2)
this.setState({visible: false,});
};
Child
<Modal
title={t('preferenceConfirmTitle')}
visible={this.props.visible}
onOk={this.props.onOk}
Thank you
You can pass the function located on the parent to the child. Here's an example:
// Parent.js
import React, {useState} from "react";
import Child from "./Child";
const Parent = () => {
const [myState, setMyState] = useState();
const funcOnlyInParent = () => {
console.log("I'm only in the parent");
}
const doSomething = (argsFromChild) => {
setMyState(argsFromChild);
funcOnlyInParent();
}
return (
<div>
<Child handleDoSomething={doStomething} />
</div>
)
}
// Child.js
import React, {useState} from "react";
const Child = (props) => {
return (
<div>
<button onClick={() => props.handleDoSomething("my arguments")}>Click</button>
</div>
)
}
The Child component will trigger the doSomething function in the parent and pass in any arguments that it needs to. Because the function is defined in the Parent component, then it can also perform actions on data that only the parent has access to - like setting the parent's state.
const Parent = () => {
const function1 = () => {
// your local code
}
return (
<Modal parentFunction={function1} />
)
}
const Modal = ({parentFunction}) => {
const onOK = () => {
// your local code
parentFunction()
}
return (
<button onClick={onOK}>Click</button>
)
}

Adding refs to components passed in an array

I'm trying to make a component that takes a list of other components as a prop and then renders one of those components as a child according to an index held in the parent's state.
Ultimately I want to be able to call the 'getValidation' function of a child element in the array using imperative handler and forwardRef methodology. I've done this for a single child component but can't figure out how to do it with an array of children. I thought about creating an array of refs in the parent component but couldn't get that right. Would appreciate any help and alternative ways of going about this are more than welcome.
E.g.
Parent:
import React, {createRef, useRef, useEffect, useState} from 'react';
const Parent = props => {
const [currentChildIndex, setCurrentChildIndex] = useState(0);
return (
<div className='parent'>
{
props.children[currentChildIndex]
}
</div>
)
};
export default Parent;
Child:
import React, {forwardRef, useEffect, useImperativeHandle, useRef} from 'react';
const Child = forwardRef((props, ref) => {
isValidated() {
//stuff that validates the form
return true;
}
useImperativeHandle(ref, () => ({
getValidation() {
return isValidated();
}
}));
return (
<div className='child'>
{form with inputs and things}
</div>
)
});
export default Child;
So I'm not sure if this is the best way to do it but I was having trouble asigning the refs to my array from inside the render method of the Parent component so I made a function to render the child component and called that from the render method and it seems to be working:
import React, {createRef, useRef, useEffect, useState} from 'react';
const Parent = props => {
const [currentChildIndex, setCurrentChildIndex] = useState(0);
function renderChildComponent(CurrentComponent) {
return (
<CurrentComponent ref={childRefs[currentChildIndex] = createRef()} />
)
}
return (
<div className='parent'>
{
renderChildComponent(props.children[currentChildIndex])
}
</div>
)
};
export default Parent;

2 way event-binding between parent and child components is not working

Working with an array of mapped items, I am attempting to toggle class in a child component, but state change in the parent component is not passed down to the child component.
I've tried a couple different approaches (using {this.personSelectedHandler} vs. {() => {this.personSelectedHandler()} in the clicked attribute, but neither toggled class successfully. The only class toggling I'm able to do affects ALL array items rendered on the page, so there's clearly something wrong with my binding.
People.js
import React, { Component } from 'react';
import Strapi from 'strapi-sdk-javascript/build/main';
import Person from '../../components/Person/Person';
import classes from './People.module.scss';
const strapi = new Strapi('http://localhost:1337');
class People extends Component {
state = {
associates: [],
show: false
};
async componentDidMount() {
try {
const associates = await strapi.getEntries('associates');
this.setState({ associates });
}
catch (err) {
console.log(err);
}
}
personSelectedHandler = () => {
const currentState = this.state.show;
this.setState({
show: !currentState
});
};
render() {
return (
<div className={classes.People}>
{this.state.associates.map(associate => (
<Person
name={associate.name}
key={associate.id}
clicked={() => this.personSelectedHandler()} />
))}
</div>
);
}
}
export default People;
Person.js
import React from 'react';
import classes from './Person.module.scss';
const baseUrl = 'http://localhost:1337';
const person = (props) => {
let attachedClasses = [classes.Person];
if (props.show) attachedClasses = [classes.Person, classes.Active];
return (
<div className={attachedClasses.join(' ')} onClick={props.clicked}>
<img src={baseUrl + props.photo.url} alt={props.photo.name} />
<p>{props.name}</p>
</div>
);
};
export default person;
(Using React 16.5.0)
First of all, in your People.js component, change your person component to:
<Person
name={associate.name}
key={associate.id}
clicked={this.personSelectedHandler}
show={this.state.show}}/>
You were not passing the prop show and also referring to a method inside the parent class is done this way. What #Shawn suggested, because of which all classes were toggled is happening because of Event bubbling.
In your child component Person.js, if you change your onClick to :
onClick={() => props.clicked()}
The parenthesis after props.clicked executes the function there. So, in your personSelectedHandler function, you either have to use event.preventDefault() in which case, you also have to pass event like this:
onClick={(event) => props.clicked}
and that should solve all your problems.
Here's a minimal sandbox for this solution:
CodeSandBox.io

Categories