I'm running into an issue with setState and I'm not exactly sure what I'm doing incorrectly.
I'll provide the full code below.
I start off with a function class to call my API to pull data from the database, then pass this to my Applicants Class. The Applicants Class is a datatable with the far right column being an edit button to bring a modal popup which will include a form to update any applicants in my table. (image below)
As you can see from my imports, I'm using ReactTable v7.7, so I'm able to set custom headers in my datatable. I'm trying to add a button that calls my classes ToggleModal function. This will set the modalDisplayBool to true (so the modal is visible) and then pass in that rows data. When the Modal is visible, I have a button to close it which calls the same ToggleModal function to hide the modal and reset the rows data.
My Modal 'X' button to close the modal works perfectly fine. It is the 'Edit' button in my Header that doesn't seem to work. When I click this, I receive the following error in my console:
Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to 'this.state' directly or define a 'state = {};' class property with the desired state in the Applicants component.
PS. I've also tried binding the class function in the constructor and binding in the onClick, but this doesn't change anything that I can see. Additionally, since I'm using React v17, it's not advised to use ComponentDidMount or any of the sort as they are antipatterns. However, I will note that I tried using the SAFE_ version, and this still didn't change my out come.
import React from 'react'
import { useApi, handleHttpServiceCallback } from '../common/services/HttpServices'
import { ReactTableBasicApplicantsPage } from '../common/datatable/ReactTable'
import Modal from '../common/modal/Modal'
import Button from '#material-ui/core/Button'
export default function ApplicantsPage() {
const url = '/users/applicants'
const { data, isLoading, hasError, httpStatus, redirectURL } = useApi(url, {initialLoad: true}, { credentials: 'include' })
var responseFromCallback = handleHttpServiceCallback(data, isLoading, hasError, httpStatus, redirectURL)
if(responseFromCallback)
return responseFromCallback
return (
<Applicants data={data}/>
)
}
class Applicants extends React.Component {
constructor(props) {
super(props)
this.state = {
modalDisplayBool: true, // Will be false initially, but I can't get this value to be true when clicking the 'Edit' button, so setting it to true for now to test
selectedRowData: {},
data: props.data
}
const headerCol = {
Header: () => null, // No header
id: 'edit_applicant',
sortable: false,
filterable: false,
Cell: ({ row }) => (
<span>
<Button variant="outlined" color="primary" onClick={() => this.toggleModal(row.original)}> // This Doesn't Work
Edit
</Button>
</span>
)
}
const headerFound = this.state.data.column.some(x => x.id === headerCol.id)
if(!headerFound)
this.state.data.column.push(headerCol)
}
toggleModal = (selectedRowData) => {
this.setState({
modalDisplayBool: !this.state.modalDisplayBool,
selectedRowData
})
}
render() {
return (
<>
<h1>Applicants Page</h1>
<ReactTableBasicApplicantsPage columns={this.state.data.column} dataIn={this.state.data.data}/>
<Modal show={this.state.modalDisplayBool}>
<div className="ttp-modal-main">
<div className="ttp-modal-header">
<h1>Modal Header</h1>
<button
type="button"
className="btn btn-danger"
onClick={() =>this.toggleModal({})} // This Works
>
<span aria-hidden="true">×</span>
</button>
</div>
<section className="ttp-modal-content">
<p>This is the main content of the modal</p>
</section>
<section className="ttp-modal-footer">
<div>Footer area</div>
</section>
</div>
</Modal>
</>
)
}
}
With help from #vishnu-shekhawat, I fixed my issue.
As mentioned in their comment, I simply moved my headerCol variable into my classes render. See code below
class Applicants extends React.Component {
constructor(props) {
super(props)
this.state = {
modalDisplayBool: true,
selectedRowData: {},
data: props.data
}
}
toggleModal = (selectedRowData) => {
this.setState({
modalDisplayBool: !this.state.modalDisplayBool,
selectedRowData
})
}
render() {
// vvvv Moving this here fixed my issue
const headerCol = {
Header: () => null, // No header
id: 'edit_applicant',
sortable: false,
filterable: false,
Cell: ({ row }) => (
<span>
<Button variant="outlined" color="primary" onClick={() => this.toggleModal(row.original)}>
Edit
</Button>
</span>
)
}
const headerFound = this.state.data.column.some(x => x.id === headerCol.id)
if(!headerFound)
this.state.data.column.push(headerCol)
return (
<>
<h1>Applicants Page</h1>
<ReactTableBasicApplicantsPage columns={this.state.data.column} dataIn={this.state.data.data} />
<Modal show={this.state.modalDisplayBool}>
<div className="ttp-modal-main">
<div className="ttp-modal-header">
<h1>Modal Header</h1>
<button
type="button"
className="btn btn-danger"
onClick={() =>this.toggleModal({})}
>
<span aria-hidden="true">×</span>
</button>
</div>
<section className="ttp-modal-content">
<p>This is the main content of the modal</p>
</section>
<section className="ttp-modal-footer">
<div>Footer area</div>
</section>
</div>
</Modal>
</>
)
}
}
Related
I've tried to look everywhere and couldn't find anything related to my use case, probably I'm looking for the wrong terms.
I have a situation where I have a bar with 3 icons, I'm looking for set one icon "active" by changing the class of it.
The icon is a custom component which have the following code
export default class Icon extends Component {
state = {
selected : false,
}
setSelected = () => {
this.setState({
selected : true
})
}
setUnselected = () => {
this.setState({
selected : false
})
}
render() {
var classStatus = '';
if(this.state.selected)
classStatus = "selected-icon"
else
classStatus = "icon"
return <div className={classStatus} onClick={this.props.onClick}><FontAwesomeIcon icon={this.props.icon} /></div>
}
}
In my parent component I have the following code
export default class MainPage extends Component {
handleClick(element) {
console.log(element);
alert("Hello!");
}
render() {
return (
<div className="wrapper">
<div className="page-header">
<span className="menu-voice">File</span>
<span className="menu-voice">Modifica</span>
<span className="menu-voice">Selezione</span>
</div>
<div className="page-main">
<span className="icon-section">
<div className="top-icon">
<Icon icon={faFileCode} onClick={() => this.handleClick(this)} />
<Icon icon={faCodeBranch} onClick={() => this.handleClick(this)} />
<Icon icon={faMagnifyingGlass} onClick={() => this.handleClick(this)} />
</div>
</span>
<span className="files-section">Files</span>
<span className="editor-section"></span>
</div>
<div className="page-footer">
Footer
</div>
</div>
);
}
}
What I'm trying to achieve is that when one of the Icon child component get clicked it will set the selected state to true manage by the parent component, in the same time while one of them is true I would like that the parent would set to false the other twos.
I've tried to use the useRef function but it doesn't look as a best practise.
Which is the correct way to do it? Sending also this to the handleClick function it just return the MainPage class instead of the child. Any suggestion at least where I should watch?
Thanks in advance
I suggest not storing the state in the icon, since it doesn't know what else you're using it for. Simply have the icon component take it's 'selected' status from props. e.g.
export default class Icon extends Component {
render() {
var classStatus = '';
if(this.props.selected)
classStatus = "selected-icon"
else
classStatus = "icon"
return (
<div className={classStatus} onClick={this.props.onClick}>.
<FontAwesomeIcon icon={this.props.icon} />
</div>
);
}
};
Then you can just manage the state in the parent where it should be:
export default class MainPage extends Component {
constructor(props) {
super(props);
this.state = { selectedOption : '' };
}
handleSelectOption(newValue) {
this.setState({ selectedOption: newValue });
}
isSelected(value) {
return value === this.state.selectedOption;
}
render() {
return (
<div className="wrapper">
{ /* etc... */ }
<div className="page-main">
<span className="icon-section">
<div className="top-icon">
<Icon
icon={faFileCode}
onClick={() => this.handleSelectOption("File")}
selected={isSelected("File")}
/>
<Icon
icon={faCodeBranch}
onClick={() => this.handleSelectOption("Modifica")}
selected={isSelected("Modifica")}
/>
{ /* etc... */ }
</div>
</span>
</div>
{ /* etc... */ }
</div>
);
}
};
You should define a constructor in your class component:
constructor(props) {
super(props);
this.state = { selected : false };
}
You also have to call a function which modify the state when you click on the Icon. onClick={this.props.onClick} doesn't change the state
I have a table of accounts that contains an edit option. When the edit option is clicked, an account edit modal dialog is displayed. The user has the option to close or cancel the edit by clicking the "X" in the top right corner or clicking a Close button. When the modal closes there are state properties that i would like to clear, both dialog properties and parent component properties. The dialog properties are updated without any issues but i receive this error when the parent properties are attempted to be updated:
Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
This is the parent component:
export class Accounts extends Component {
constructor(props, context) {
super(props);
this.state = {
loading: true,
showEditUserModal: false,
userId: null,
redraw: false,
}
this.handleAccountEdit = this.handleAccountEdit.bind(this);
}
handleAccountEdit(cell, row, rowIndex) {
this.setState({ userId: cell },
this.setState({ showEditUserModal: true }));
}
//This is parent function being called from dialog close Error occurs here
handleAccountEditClose() {
this.setState({ userId: null, showEditUserModal: false, redraw: true });
}
render() {
return (
<div className='container-fluid'>
{this.state.showEditUserModal ?
<AccountEditModal userId={this.state.userId}
parentCloseAccountEditModal={() => this.handleAccountEditClose()}></AccountEditModal>
</div>
)
}
AccountEditModal:
export default class AccountEditModal extends React.Component {
constructor(props, context) {
super(props);
this.state = {
uId: null,
userName: '',
showModal: true,
}
}
handleClose = (e) => {
this.setState({ showModal: false, uId: null, userName: '' });
}
render() {
return (
<div >
<Modal show={this.state.showModal} onHide={() => { this.handleClose(); this.props.parentCloseAccountEditModal() }} centered >
<Modal.Header closeButton>
<Modal.Title>Account Edit</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="row">
<div className="row pad-top float-right">
<div className='col-md-2 my-auto'>
<span id='btnCloseAccountEdit' onClick={() => { this.handleClose(); this.props.parentCloseAccountEditModal() }}
className='btn btn-success'>Close</span>
</div>
</div>
</div>
</Modal.Body>
</Modal>
</div>
)
}
How can i update the parent component properties without getting this error?
The suggested solution does not call a parent component function. I changed the handleClose to a lambda in the AccountEditModal but still receive the same error.
I am nit sure what you are doing different than I am but based on your code and explanation I created this demo application and it is working as expected without any errors. May be you might want to compare it to your application and check what is different in your app.
Find working codesandbox here.
Accounts.js
import React, { Component } from "react";
import { AccountEditModal } from "./AccountEditModal";
export class Accounts extends Component {
constructor(props, context) {
super(props);
this.state = {
loading: true,
showEditUserModal: false,
userId: null,
redraw: false
};
this.handleAccountEdit = this.handleAccountEdit.bind(this);
}
handleAccountEdit(cell) {
this.setState({ userId: cell }, this.setState({ showEditUserModal: true }));
}
//This is parent function being called from dialog close Error occurs here
handleAccountEditClose() {
this.setState({ userId: null, showEditUserModal: false, redraw: true });
}
render() {
return (
<div className="container-fluid">
{this.state.showEditUserModal ? (
<AccountEditModal
userId={this.state.userId}
parentCloseAccountEditModal={() => this.handleAccountEditClose()}
></AccountEditModal>
) : (
<table>
{this.props.users.map((uId) => {
return (
<tr>
<td>
<button onClick={() => this.handleAccountEdit(uId)}>
{uId}
</button>
</td>
</tr>
);
})}
</table>
)}
</div>
);
}
}
AccountEditModal.js
import React, { Component } from "react";
import { Modal } from "react-bootstrap";
export class AccountEditModal extends Component {
constructor(props, context) {
super(props);
this.state = {
uId: null,
userName: "",
showModal: true
};
}
handleClose = (e) => {
this.setState({ showModal: false, uId: null, userName: "" });
};
render() {
return (
<div>
<Modal
show={this.state.showModal}
onHide={() => {
this.handleClose();
this.props.parentCloseAccountEditModal();
}}
centered
>
<Modal.Header closeButton>
<Modal.Title>Account Edit: {this.props.userId}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="row">
<div className="row pad-top float-right">
<div className="col-md-2 my-auto">
<span
id="btnCloseAccountEdit"
onClick={() => {
this.handleClose();
this.props.parentCloseAccountEditModal();
}}
className="btn btn-success"
>
Close
</span>
</div>
</div>
</div>
</Modal.Body>
</Modal>
</div>
);
}
}
I have added dummy application data in App.js:
import React, { useState } from "react";
import { Accounts } from "./Accounts";
import "./styles.css";
export default function App() {
const [users] = useState([
"User1",
"User2",
"User3",
"User4",
"User5",
"User6",
"User7",
"User8",
"User9",
"User10"
]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Accounts users={users}></Accounts>
</div>
);
}
One easy way to reset your component is to manage it with a key. See: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
More precisely:
When a key changes, React will create a new component instance rather
than update the current one.
So following this, you could for example use a timecode so that every time you call this modal window a new instance is created with default values. Like this:
<AccountEditModal
key={Date.now()}
userId={this.state.userId}
parentCloseAccountEditModal={() => this.handleAccountEditClose()}>
</AccountEditModal>
Then you don't need this function and this call: this.handleClose(); Simply call this one: this.props.parentCloseAccountEditModal() and next time you call the modal, you're going to have a new instance.
For normal components, it may introduce non wanted behavior, but for a modal window it's usually exactly the intent: each time you close/open it, you want a reset state, and you're not going to change the props except by opening it - since it's modal and so prevents interactions with the parent.
I have info button that is supposed to open specific description element onclick event - info is obtained from Firebase. However, myOnclick event triggers all of the siblings elements and I need to toggle/untoggle only specific one. What am I missing and doing wrong?
here's the code:
import React, { Component } from "react";
import firebase from "../../firebase";
//Data obtained from DB and rendered on page
export default class Tour extends Component {
constructor() {
super();
this.state = {
tours: [],
showInfo: false,
};
}
// button that toggles info
handleInfo = () => {
this.setState({
showInfo: !this.state.showInfo,
});
};
// component did mount
componentDidMount() {
const dbRef = firebase.database().ref();
dbRef.on("value", (snapshot) => {
// checking changes in db
const data = snapshot.val();
const newToursAarray = [];
for (let inventoryName in data) {
const toursObject = {
id: inventoryName,
tours: data[inventoryName],
name: data[inventoryName].name,
seats: data[inventoryName].seats,
date: data[inventoryName].date,
duration: data[inventoryName].duration,
imgUrl:"https://source.unsplash.com/350x350/?" + data[inventoryName].name,
// temporary tour info placeholder and will be removed and connetcted to real DB
info: "Lorem ipsum dolora saepe fugiat. " +
data[inventoryName].name,
};
newToursAarray.push(toursObject);
}
this.setState({
tours: newToursAarray,
});
});
}
render() {
return (
<div className="tourlist">
{this.state.tours.map((toursObject) => {
return (
<section className="tourItem">
<header>
<h3> {toursObject.name} </h3>
<h5>
info
{/* button that toggles info */}
<span onClick={this.handleInfo}>
<i className="fas fa-caret-square-down"></i>
</span>
</h5>
</header>
<ul className="inventoryItem" key={toursObject.id}>
<li> {toursObject.date} |</li>
<li>{toursObject.duration} hrs |</li>
<li> {toursObject.seats} seats </li>
</ul>
<div className="img-container">
{this.state.showInfo && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
<img src={toursObject.imgUrl} alt="image of the tour" />
</div>
</section>
);
})}
</div>
);
}
}
So the issue here is that the expanding options are all referencing the same piece of state. You have your state.showInfo outside of your tours.map. So as you map through your tours, you say "if 'showInfo' then reveal this section" and that 'showInfo' is the same for everything.
What I would recommend is storing the id of the expanded tourObject, and then you can check against that.
Something like this:
handleInfo = (id) => {
this.setState({
infoToShow: id,
});
};
And then in your onClick it would look more like this:
<span onClick={() => this.handleInfo(tourObject.id)}>
<i className="fas fa-caret-square-down"></i>
</span>
And your logic to show or hide can just be this:
{this.state.infoToShow === tourObject.id && (
// text that toggles when clicking on info button
<p className="tour-info">{toursObject.info}</p>
)}
This way you can store the id in a place accessible to all the looped through tours, but they won't conflict with each other.
So instead of checking if showInfo is true, check if the id that you want to show matches the id of the tour.
I have an alert component that is invisible by default, and when something happens (e.g. click) in the parent component the alert component will be visible. Inside the alert component, there is a close button to make the component invisible again. I am new to react and I found it quite difficult to implement.
Here is my current solution:
The alert component:
import React, {useEffect, useState} from "react";
const Alert = ({show, style, message}) => {
const [visibility, setVisibility] = useState(show);
useEffect(() => {
setVisibility(show);
},[show]);
return (
<>
{(visibility)?(
<div className="col">
<div className={"alert shadow alert-"+style} role="alert">
{message}
<button type="button" className="close" aria-label="Close" onClick={(e) => {
setVisibility(false);
}}>
<span aria-hidden="true">×</span>
</button>
</div>
</div>
):(<></>)}
</>
);
}
export default Alert;
The parent component has a state to manage it.
const [alert, setAlert] = useState({
'show': false,
'style':'',
'message':''
});
<Alert show={alert.show} style={alert.style} message={alert.message} />
<button onClick={(e) => {
setAlert({
'show':false,
'style':'',
'message':''
});
setAlert({
'show':true,
'style':'success',
'message':'Thank you!'
});
}>
show
</button>
The problem is that because the prop show is not changed when clicking on the close button inside the alert component when I click on the button in the parent component, the prop show is still true so it won't render the alert component. The workaround I am using is to set the alert prop show to false before setting it to true again. This is not very neat. Is there a better way to combine the prop and the state to decide the visibility of the alert?
You need to define a function to hide the Alert in the parent component itself and pass it down to the child component.
const hideAlert = () => {
setAlert(prevState => ({
...prevState,
show: !prevState.show
}))
}
Then you can pass this to Alert component,
<Alert show={alert.show} style={alert.style} message={alert.message} hideAlert={hideAlert}/>
In the alert component, no need to store props into the state again. You should directly use props,
const Alert = ({show, style, message, hideAlert}) => {
return (
<>
{ show ? <div className="col"> //directly use props here to show alert
<div className={"alert shadow alert-"+style} role="alert">
{message}
<button type="button" className="close" aria-label="Close" onClick={hideAlert}>
<span aria-hidden="true">×</span>
</button>
</div>
</div>
:
<></>
}
</>
);
}
Demo
I have added a click handler so that when a user clicks outside of my modal, it closes the modal box. I am using 'react-outside-click-handler' in my Gatsby.js project to achieve this.
This click handler is working perfectly and closes the modal when cliked outside of the box. However, it also toggles the modal to activate if clicked anywhere on the page (when the modal is not active).
Could someone point me in the right direction as to how to stop the activation of the modal whilst keeping the deactivation feature?
Perhaps I could write an if statement specifying that when the state is false, the outside clicks do not toggle the modal?
The page:
export default class Contact extends Component {
state = {
modal: false,
}
modalToggle = () => {
this.setState({ modal: !this.state.modal })
}
render = () => {
return (
<div className="temp_background">
<div className="work_boxes">
<OutsideClickHandler onOutsideClick={this.modalToggle}>
<button
className="place-order"
style={{ backgroundImage: `url(${work_screenshot_2})` }}
onClick={this.modalToggle}
/>
<Modal onClick={this.modalToggle} status={this.state.modal} />
</OutsideClickHandler>
</div>
</div>
)
}
}
The modal component:
export default class Modal extends Component {
render() {
return (
<div>
<div className="modal" data-status={this.props.status}>
<div
className="modal-left"
style={{ backgroundImage: `url(${work_screenshot_2})` }}
/>
<div className="modal-right">
<h2>{this.props.title}</h2>
<p>{this.props.description}</p>
<button onClick={this.props.onClick} className="close">
<span className="fa fa-close">x</span>
</button>
</div>
</div>
</div>
)
}
}
You could conditionally render it but you should also look at being more explicit with your function calls here. Using a toggleModal call may be more concise, but being less explicit comes at a cost.
By having two separate methods: openModal and closeModal, you can more clearly see what your code is doing. Now, your OutsideClickHandler component explicitly closes the modal if you click outside of it. The methods additionally check to make sure the modal is opened/closed before making changes.
export default class Contact extends Component {
state = {
modal: false,
}
openModal = () => {
const { modal } = this.state;
if (!modal) {
this.setState({ modal: true })
}
}
closeModal = () => {
const { modal } = this.state;
if (modal) {
this.setState({ modal: false })
}
}
render = () => {
return (
<div className="temp_background">
<div className="work_boxes">
<OutsideClickHandler onOutsideClick={this.closeModal}>
<button
className="place-order"
style={{ backgroundImage: `url(${work_screenshot_2})` }}
onClick={this.openModal}
/>
<Modal onClick={this.closeModal} status={this.state.modal} />
</OutsideClickHandler>
</div>
</div>
)
}
}
You could conditionally render it:
render = () => {
return (
{this.state.modal ? (<div className="temp_background">
<div className="work_boxes">
<OutsideClickHandler onOutsideClick={this.modalToggle}>
<button
className="place-order"
style={{ backgroundImage: `url(${work_screenshot_2})` }}
onClick={this.modalToggle}
/>
<Modal onClick={this.modalToggle} status={this.state.modal} />
</OutsideClickHandler>
</div>
</div>) : null }
)
}
As #LMulvey points out, the proper way would be to have separate open and close handlers, and it should definitely be done that way in more complicated situations. But for this simple case, a toggle is fine I think.