React Custom Modal Component Problems With Updating Same Component Used Repeatedly - javascript

I built a custom Modal.
There is one particular function I would like it to do when opened. I would like a CSS class to be toggled when this modal is opened/closed.
This works just fine if I only insert this component once in a template. But in my case I am inserting it three times. By using the componentDidMount I insert some JS that should toggle the CSS class. It does not do it for the first or the second modal, it will only do it for the third.
CODE UPDATED!
This is the parent component:
import React from "react";
import ModalSmall from "./ModalSmall";
import ModalMedium from "./ModalMedium";
import ModalLarge from "./ModalLarge";
import "bootstrap/dist/css/bootstrap.css";
import "./styles.scss";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
};
}
toggleModalSmall = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}));
};
toggleModalMedium = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalMediumOpen: !prev.isModalMediumOpen
}));
};
toggleModalLarge = (e) => {
e.preventDefault();
this.setState((prev) => ({
...prev,
isModalLargeOpen: !prev.isModalLargeOpen
}));
};
render() {
return (
<div className="container">
<div className="row">
<div className="col">
<h1>Hello Y'all!</h1>
<p className="yo-green">My Modal Samples</p>
<div className="row mt-5">
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalSmall}
>
Modal Small
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalMedium}
>
Modal Medium
</button>
</div>
<div className="col">
<button
className="btn btn-primary"
onClick={this.toggleModalLarge}
>
Modal Large
</button>
</div>
</div>
</div>
</div>
<ModalSmall
modalName="smallModal"
modalTitle="Small Modal"
modalBody="This is the small modal!"
toggleModal={this.toggleModalSmall}
modalOpen={this.state.isModalSmallOpen}
/>
<ModalMedium
modalName="mediumModal"
modalTitle="Medium Modal"
modalBody="This is the medium modal!"
toggleModal={this.toggleModalMedium}
modalOpen={this.state.isModalMediumOpen}
/>
<ModalLarge
modalName="largeModal"
modalTitle="Large Modal"
modalBody="This is the LARGE modal!"
toggleModal={this.toggleModalLarge}
modalOpen={this.state.isModalLargeOpen}
/>
</div>
);
}
}
One of the in-between components:
import React from "react";
import Modal from "./Modal";
const ModalSmall = (props) => {
return (
<Modal
modalName={props.modalName}
modalTitle={props.modalTitle}
modalBody={props.modalBody}
toggleModal={props.toggleModal}
modalOpen={props.modalOpen}
/>
);
};
export default ModalSmall;
Here is my modal Component:
import React from "react";
export default class Modal extends React.Component {
componentDidUpdate() {
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
render() {
return (
<div className="mymodal" id={this.props.modalName}>
<div
onClick={this.props.toggleModal}
className={`mymodal-overlay ${this.props.modalOpen && "active"}`}
></div>
<div
className={`mymodal-content d-flex flex-column ${
this.props.modalOpen && "active"
}`}
>
<header className="p-2 border-bottom d-flex">
<span
className="material-icons clickable"
onClick={this.props.toggleModal}
>
close
</span>
<div className="flex-grow-1 ml-2">{this.props.modalTitle}</div>
</header>
<div className="p-2 flex-grow-1">{this.props.modalBody}</div>
<footer className="p-2 border-top">© ChidoPrime 2021</footer>
</div>
</div>
);
}
}
Working Sample Here with Solution Applied
UPDATE! -------------
There is a second approach I would like to include, different than the checked answer offered by #sanishJoseph. In which I add a constructor and declare a state within the modal controller. Without the need of using React.PureComponent. I use preProvs within the componentDidUpdate. Code for the modal follows:
constructor(props) {
super(props);
this.state = {
modalOpen: false
};
}
componentDidUpdate(prevProps) {
if (prevProps.modalOpen === this.props.modalOpen) return;
if (this.props.modalOpen) {
console.log("Open!", this.props.modalOpen);
document.body.classList.add("drawer-open");
} else {
console.log("Closed!", this.props.modalOpen);
document.body.classList.remove("drawer-open");
}
}
Second Sample using prevProps without using React.PureComponent

I think the biggest mistake is in your Parent component. Your initial state of the page is
this.state = {
isModalSmallOpen: false,
isModalMediumOpen: false,
isModalLargeOpen: false
}
But, when you open a Modal, you are setting your state to one item in the state, rest of the items are going null. Meaning, when you do
this.setState({
isModalSmallOpen: !this.state.isModalSmallOpen
})
You are setting isModalMediumOpen: null, isModalLargeOpen: null.
What you should be doing is,
this.setState((prev) => ({...prev,
isModalSmallOpen: !prev.isModalSmallOpen
}))
So all of your states will remain in your state. This change is needed in all the 3 modal opening functions.
Update :
Fix is petty easy. All you need to do is add a react.memo if it was a functional component. In your case make your Modal component as a PureComponent.
export default class Modal extends React.PureComponent
Pure Components in React are the components which do not re-renders
when the value of state and props has been updated with the same
values.
https://codesandbox.io/s/my-custom-modal-forked-yg4vo?file=/src/App.js

The code is a little complex to understand, but I think the main problem is with the logic used to implement it. If I understood correctly you are using the same Component more than once. So, each component executes componentDidUpdate method each time that is rerendered.
What this means is that if you are toggling one of your modals in the "parent" component with the methods "toggleModal..." then, the parent render method is executed and it executes each render children method. What happened there is that with your first modal you are adding o removing the body css, with the second you are doing the inverse and with the third, you are adding and removing again.
You have a lot of things to get better there, but the most simple is use the arguments you got in your componentDidUpdated method and make sure you only executed your code if the new props changes. This going to solve your problem.

Related

How should I render multi-state component, which fetches API?

I am using React with Node.js.
I have a component ItemList, which fetches some API in the componentDidMount() method, because it allows me to easily render a "loading state".
I need to pass a state to this component, which would change the API's url using a toggle button. This toggle button is an individual component (ToggleButton). These two components are siblings and I am using parent as a way for these components to communicate.
I thought Context is perfect for this kind of job. The issue is, that using React's Context is just re-rendering (calling the function render() of a ItemList and not remounting the component, thus not calling the componentDidMount() method or even constructing the component again).
export const ToggleContext = React.createContext({
switched: false
});
export default class Items extends React.Component
{
constructor(props)
{
super(props)
this.state = {
switched: false
}
this.toggleSwitch = () => {
this.setState(state => ({
switched: !state.switched
}))
}
}
render()
{
console.log(this.state.switched)
return (
<>
<div class="page-header">
<div class="container-fluid">
<h2 class="h5 mb-0">Items</h2>
</div>
</div>
<section class="py-0">
<div class="container-fluid">
<ToggleContext.Provider value={this.state.switched}>
<ToggleButton callFunc={this.toggleSwitch}/>
<ItemList loadWeb={this.state.switched}/>
</ToggleContext.Provider>
</div>
</section>
</>
)
}
}
ItemList component is heavily inspirated by React docs
I am succesfully getting the changed state through ToggleButton in parent component, sending it to ItemList and picking it up as props.loadWeb, I am just not sure if my implementation is wrong or even if what I demand is possible with Context.
Is it possible to reconstruct the whole component using context, should I use refs, sould I fetch the API in the render() method, etc.?
So it appears that you need your componentDidMount called on every toggle of ItemList. But as you noted, it does not remount every time. As such, using componentDidUpdate inside your ItemList is more appropriate to toggle that API link.
Docs: https://reactjs.org/docs/react-component.html#componentdidupdate
I also encourage you to check out this article on using Hooks vs Classes: https://blog.bitsrc.io/6-reasons-to-use-react-hooks-instead-of-classes-7e3ee745fe04
What you are doing seems right, I would say, you could probably simplify (in my opinion) by using function components along with hooks.
import React, {useState} from 'react';
const Items = () => {
const [toggled, toggle] = useState(false);
return (
<div class="container-fluid">
<div class="page-header">
<h2 class="h5 mb-0">Items</h2>
</div>
<section class="py-0">
<ToggleButton callFunc={toggle(!toggled)}/>
<ItemList loadWeb={toggled}/>
</section>
</div>
);
};

Odd behavior with react-modal

I'm trying to build a quiz that uses react-modal to provides hints. I will need multiple modals inside the quiz. I'm new to React so it's quite possible that I'm making a simple mistake.
I'm not sure it matters, but I've built this using create-react-app.
My App.js looks like this:
import React, { Component } from 'react';
import HintModal from './hintModal';
import Modal from 'react-modal';
import './App.css';
Modal.setAppElement('#root');
class App extends Component {
state = {
modalIsOpen: false,
hint: ''
};
openModal = (hint) => {
this.setState({ modalIsOpen: true, hint: hint });
}
closeModal = () => {
this.setState({ modalIsOpen: false, hint: '' });
}
render() {
return (
<React.Fragment>
<h1>Modal Test</h1>
<h2>First Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="mango"
/>
<hr />
<h2>Second Modal</h2>
<HintModal
modalIsOpen={this.state.modalIsOpen}
openModal={this.openModal}
closeModal={this.closeModal}
hint="banana"
/>
</React.Fragment>
);
}
}
export default App;
hintModal.jsx looks like this:
import React, { Component } from 'react';
import Modal from 'react-modal';
const HintModal = (props) => {
const {openModal, modalIsOpen, closeModal, hint} = props;
return (
<React.Fragment>
<button onClick={ () => openModal(hint) }>Open Modal</button>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="Example Modal"
>
<h2>Hint</h2>
<p>{hint}</p>
<button onClick={closeModal}>Close</button>
</Modal>
<p>We should see: {hint}</p>
</React.Fragment>
);
};
export default HintModal;
Here's the problem: I need the content of the modal to change based on the hint prop passed to HintModal. When I output hint from outside <Modal>, it behaves as expected, displaying the value of the prop. But when I output hint within <Modal>, it returns "banana" (the value of the hint prop for the second instance of HintModal) when either modal is activated.
Any help would be greatly appreciated!
You are controlling all of your modals with the same piece of state and the same functions to open and close the modal.
You need to either have just one modal and then dynamically render the message inside it or you need to store a modalIsOpen variable in your state for every single modal.

this.setState React bind issue

I am attempting to pass the changePage class method into a child component called SideBar. When the changePage method is then triggered by an onClick event in the child component I receive the following error:
Uncaught TypeError: this.SetState is not a function
From what I could find in other similar posts I need to bind the changePage method to this. I have done that but I still can't manage to get is to work.
I also saw many suggestions to use ES6 arrow functions for my methods but I get the exact same error message if I do.
I'm still quite new at web development and any help would be appreciated.
Parent Component called Main:
import React from 'react';
import Content from './Content';
import Sidebar from './Sidebar';
export default class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedPage: 'home',
pages: ['home','about','skills','contact'],
};
this.changePage = this.changePage.bind(this);
}
changePage(page) {
console.log(page);
this.SetState({
selectedPage: page,
pages: ['home','about','skills','contact']
});
};
render() {
return (
<div>
<div id="sidebar" className="side-bar">
<Sidebar
changePage={this.changePage}
selectedPage={this.state.selectedPage}
pages={this.state.pages}
/>
</div>
<div id="main" className="main-content">
<Content
selectedPage={this.state.selectedPage}
/>
</div>
</div>
)
}
}
Child Component:
import React from 'react';
export default class Sidebar extends React.Component {
render() {
console.log("content props",this.props);
const buttons = this.props.pages.map(button =>
<span
className='nav-button'
id={button}
key={button}
onClick={() => this.props.changePage(button)}
>
<img src={`./app/images/${button}.svg`} />
</span>
);
return (
<div>
<span>
<img className='headshot' src='./app/images/headshot.jpg' />
</span>
<div className='nav-container'>
{buttons}
</div>
</div>
)
}
}
You have syntax error: this.SetState. Change it to this.setState.
The typo in setState was causing the error, but you may want consider a cleaner way to bind the parent this on the changePage prop. Your way works, but if you changed sidebar Component inclusion to:
<Sidebar
changePage={ (page) => this.changePage(page)}
selectedPage={this.state.selectedPage}
pages={this.state.pages}
/>
That may make it clearer what is going on (changePage prop is a function which takes one parameter and passes that parameter to instance method this.changePage), and removes the binding gymnastics in the constructor.

React component re-renders endlessly with onClick

I want the onClick event of the button in result.js to render my Spinner component, and have so far (kind of) gotten it to do so. At the moment, Spinner mostly has some console.log() statements, and it keeps logging "Rendered spinner." endlessly after clicking the button, about once every second.
For the record, the returned paragraph isn't being displayed, but I haven't gotten around to debugging that yet. Also, I have excluded some code in Result.js that I think is irrelevant.
For now, I just want Spinner to only render once after pressing the button. Any tips?
result.js:
import React, { Component } from "react";
import { connect } from "react-redux";
import Spinner from "./spinner";
class UnboxResult extends Component {
constructor(props) {
super(props);
this.state = {
showSpinner: false
};
this.handleUnboxClicked = this.handleUnboxClicked.bind(this);
}
handleUnboxClicked(event) {
event.preventDefault();
console.log("Inside handleUnboxClicked");
this.setState({
showSpinner: true
});
}
render() {
return (
<section className="opening">
<div className="container">
<div className="row">
<button onClick={this.handleUnboxClicked}>UNBOX</button>
</div>
<div className="row">
{this.state.showSpinner ?
<Spinner items={this.props.unbox.items}/> :
null}
</div>
</div>
</section>
);
}
}
export default connect(state => ({
unbox: state.unbox
}))(UnboxResult);
spinner.js:
import React, { Component } from 'react';
class Spinner extends Component {
constructor(props) {
console.log("Before super");
super(props);
console.log("Ran constructor.");
}
render(){
console.log("Rendered spinner.");
return(
<p>Spinning..</p>
);
}
}
export default Spinner;
You could add a handler method to update the state from spinner
handleClick(){
this.setState({
showSpinner: true
})
}
and in your render it will need to be passed as prop
<div className="row">
{this.state.showSpinner ?
<Spinner handleClick={this.handleClick}/> :
null}
</div>
In your spinner component return you can trigger this using onclick
<button onClick = {this.props.handleClick} > Click </button>
This will allow you to update the state back in your parent, You might want to figure out how you would display the items one at a time in spinner and only set state to false when there is no items left to display.
Sorry if i misunderstood your comment.

ReactJS beginner state updated but rendering in child not

Bear with me beginner at ReactJS so I am testing stuff out.
I created a static page as a component and in that component I load another custom component.
The data is coming from an ajax call and I update the state but it doesn't update the child component's view.
The static page
// Test.JS
import React from 'react';
import Layout from '../../components/Layout';
import Article from '../../components/Article';
import s from './styles.css';
import axios from 'axios';
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {
article: null
};
}
componentWillMount(){
axios.get("https://gist.githubusercontent.com/koistya/a32919e847531320675764e7308b796a/raw/articles.json")
.then(function(result) {
var theArticle = result.data.filter((article) => article.title.split(' ').join('') === this.props.route.params.title.split(' ').join(''));
this.setState({
article: theArticle[0]
})
}.bind(this));
}
render() {
return (
<Layout className={s.content}>
<Article {...this.state.article} />
</Layout>
);
}
}
export default Test;
As you can see i update the state with an ajax call but the child is not updated. the console log in Article is showing a null object because first time it renders there is nothing inside. But after updating the state I expect it should pass it through to the childeren.
import React from 'react';
import Link from '../Link';
class Article extends React.Component{
componentDidMount() {
console.log(this.props);
window.componentHandler.upgradeElement(this.root);
}
componentWillUnmount() {
window.componentHandler.downgradeElements(this.root);
}
render() {
return (
<div className="demo-card-wide mdl-card mdl-shadow--2dp" ref={node => (this.root = node)}>
<div className="mdl-card__title">
<h2 className="mdl-card__title-text">{this.props.author}</h2>
</div>
<div className="mdl-card__supporting-text">
</div>
<div className="mdl-card__actions mdl-card--border">
</div>
<div className="mdl-card__menu">
<button className="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect">
<i className="material-icons">{this.props.title}</i>
</button>
</div>
</div>
);
}
}
export default Article;
So I have the following questions:
1) First of all am I going the right direction is this how it should be done ?
2) Is a constructer not better than willmount event ?
2) Why is it now not updating the child view ?
3) Should I use a prop or state in this case (in Test.js) (still not sure after reading allot about it)
I changed it to componentDidMount and now it updates the view.
I have no idea why it didnt work before!

Categories