I'm using react-bootstrap. I'm trying to display a list of cards. The list size can vary. But entire page becomes white after the component' Websites' is added at the bottom of 'Dashboard Page'. {this.state.page === 'websites' ? this.websites : null}
Websites Component
import * as React from 'react'
import { Card } from 'react-bootstrap'
import './Websites.css'
export default class Websites extends React.Component {
constructor(props) {
super(props)
this.websites = []
this.getWebsites()
}
getWebsites () {
// TODO: Need Implementation
var websites = this.makeTestWebsites()
for (var i = 0; i < websites.length; i++) {
this.websites.push(
<Card>
<Card.Body>
<Card.SubTitle>{websites[i].domain}</Card.SubTitle>
<Card.Title>{websites[i].name}</Card.Title>
<Card.SubTitle>{websites[i].websiteClass}</Card.SubTitle>
<Card.SubTitle>{websites[i].created}</Card.SubTitle>
</Card.Body>
</Card>
)
}
}
makeTestWebsites () {
return [
{
name: 'test domain',
domain: 'domain.com',
websiteClass: 'starter',
created: '2077-12-05'
},
{
name: 'hello domain',
domain: 'chicken.org',
websiteClass: 'premium',
created: '1996-02-12'
}
]
}
render () {
return (
<div>
{this.websites}
</div>
)
}
}
is called from DashboardPage
import * as React from 'react'
import { Button } from 'react-bootstrap'
import './dashboardStyle.css'
import Websites from './Websites'
export default class DashboardPage extends React.Component {
constructor(props) {
super(props)
this.state = {
page: 'websites'
}
this.websites = <Websites />
this.switchToWebsites()
}
switchToWebsites () {
this.setState({ page: 'websites' })
}
render () {
return (
<div>
<div className='sidemenu'>
<Button variant='primary' className='sidenav-button'>
<img
alt=''
src='/logo/xxxLogo.png'
width='30'
height='30'
className='sidenav-button-icon'
onClick={this.showLandingPage}
/>
Websites
</Button>
<Button variant='primary' className='sidenav-button'>
<img
alt=''
src='/logo/xxxLogo.png'
width='30'
height='30'
className='sidenav-button-icon'
onClick={this.showLandingPage}
/>
Email
</Button>
<Button variant='primary' className='sidenav-button'>
<img
alt=''
src='/logo/xxxLogo.png'
width='30'
height='30'
className='sidenav-button-icon'
onClick={this.showLandingPage}
/>
How To Use
</Button>
<Button variant='primary' className='sidenav-button'>
<img
alt=''
src='/logo/xxxLogo.png'
width='30'
height='30'
className='sidenav-button-icon'
onClick={this.showLandingPage}
/>
Support
</Button>
</div>
<div className='right-menu'>
{this.state.page === 'websites' ? this.websites : null}
</div>
</div>
)
}
}
Sorry for little details. I'm new and don't know what info you need. If you need any details please comment. Thank you.
Edit: Error log from console
Uncaught Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
Try this:
Websites Component
import React from "react";
import { Card } from "react-bootstrap";
import "./Websites.css";
const testWebsites = [
{
name: "test domain",
domain: "domain.com",
websiteClass: "starter",
created: "2077-12-05"
},
{
name: "hello domain",
domain: "chicken.org",
websiteClass: "premium",
created: "1996-02-12"
}
];
export default class Websites extends React.Component {
constructor(props) {
super(props);
// TODO: Need Implementation
this.state = {
websites: testWebsites
};
}
render() {
return (
<div>
{this.state.websites.map(website => (
<Card key={website.name}>
<Card.Body>
<Card.SubTitle>{website.domain}</Card.SubTitle>
<Card.Title>{website.name}</Card.Title>
<Card.SubTitle>{website.websiteClass}</Card.SubTitle>
<Card.SubTitle>{website.created}</Card.SubTitle>
</Card.Body>
</Card>
))}
</div>
);
}
}
Dashboard Component
import React from "react";
import { Button } from "react-bootstrap";
import "./dashboardStyle.css";
import Websites from "./Websites";
export default class DashboardPage extends React.Component {
constructor(props) {
super(props);
this.state = {
page: "websites"
};
}
showLandingPage = () => {
this.setState({
page: "landing"
});
};
showWebsitesPage = () => {
this.setState({
page: "websites"
});
};
render() {
return (
<div>
<div className="sidemenu">
<Button
variant="primary"
className="sidenav-button"
onClick={this.showLandingPage}
>
<img
alt=""
src="/logo/BornBee Logo.png"
width="30"
height="30"
className="sidenav-button-icon"
/>
Websites
</Button>
<Button
variant="primary"
className="sidenav-button"
onClick={this.showLandingPage}
>
<img
alt=""
src="/logo/BornBee Logo.png"
width="30"
height="30"
className="sidenav-button-icon"
/>
Email
</Button>
<Button
variant="primary"
className="sidenav-button"
onClick={this.showLandingPage}
>
<img
alt=""
src="/logo/BornBee Logo.png"
width="30"
height="30"
className="sidenav-button-icon"
/>
How To Use
</Button>
<Button
variant="primary"
className="sidenav-button"
onClick={this.showLandingPage}
>
<img
alt=""
src="/logo/BornBee Logo.png"
width="30"
height="30"
className="sidenav-button-icon"
/>
Support
</Button>
</div>
<div className="right-menu">
{this.state.page === "websites" ? <Websites /> : null}
</div>
</div>
);
}
}
I made a few notable changes to your code.
Websites
Move your test data outside of the component
Use this.state instead of class properties
map over the data array. Note: the key prop must be unique (source)
Change your React import. I'm not sure if the wildcard import was a bug, but I've never seen it done like that.
Dashboard
Don't call setState in the constructor (source)
Don't store your component in a property variable
I moved your onClick to the <Button> component because that makes more sense to me. But, you can move it back to the img if you prefer or need it there.
Made your class methods arrow functions to bind this
One last thing I'd suggest is that, instead of manually handling the page routing like you're doing, perhaps use React Router.
Related
I am trying to achieve this using SPFX web part:-
The web part will render a Button and has a text field inside its settings.
The user add the Web Part >> edit it >> enter the text inside the text field (inside the setting page) >> save the web part>> then the web part will render a button >> if the user clicks on the button a popup will be shown with the entered text.
Now I found this link # https://www.c-sharpcorner.com/article/conecpt-of-react-portal-in-spfx/ which almost achieves what I am looking for, except that the Popup text inside the example is been hard-coded inside the .tsx file.. so what are the steps to make the Popup text configurable inside the web part settings instead of been hard-coded?
Thanks
Here is my ReactPortalWebPart.ts file:-
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '#microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '#microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '#microsoft/sp-webpart-base';
import * as strings from 'ReactPortalWebPartStrings';
import ReactPortal from './components/ReactPortal';
import { IReactPortalProps } from './components/IReactPortalProps';
export interface IReactPortalWebPartProps {
description: string;
}
export default class ReactPortalWebPart extends BaseClientSideWebPart<IReactPortalWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactPortalProps> = React.createElement(
ReactPortal,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
and here is the ReactPortal.tsx:-
import * as React from 'react';
import { IReactPortalProps } from './IReactPortalProps';
import Myportal from "./Myportal";
export default class ReactPortal extends React.Component<IReactPortalProps, {}> {
public render(): React.ReactElement<IReactPortalProps> {
return (
<div >
<Myportal/>
</div>
);
}
}
Here is the Myportal.tsx:-
import* as React from "react";
import usePortal from "react-cool-portal";
import "./mystyle.scss";
const Myportal = () => {
// const { Portal } = usePortal({ containerId: "my-portal-root" });
const { Portal, show, hide } = usePortal({ defaultShow: false,containerId:"my-portal-root" });
const handleClickBackdrop = (e: React.MouseEvent) => {
const { id } = e.target as HTMLDivElement;
if (id === "modal") hide();
};
return (
<div className="App">
<button className="btn" onClick={show} type="button">
Who we are
</button>
<button className="btn" onClick={show} type="button">
Our value
</button>
<Portal>
<div
id="modal"
className="modal"
onClick={handleClickBackdrop}
tabIndex={-1}
>
<div
className="modal-dialog"
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
>
<div className="modal-header">
<button
className="modal-close"
onClick={hide}
type="button"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<h1> Who we are</h1>
<h3>.................................................</h3>
Our overriding purpose is to dramatically improve the reliability, efficiency........
</div>
</div>
</div>
</Portal>
</div>
);
};
export default Myportal;
I think you need to re-write MyPortal as a React Component; bellow I'm tried written some example about this change that needed on MyPortal.tsx and ReactPortal.tsx, but I do not test then.
ReactPortal.tsx:
import * as React from 'react';
import { IReactPortalProps } from './IReactPortalProps';
import { IMyportalProps } from './IMyportalProps';
import Myportal from "./Myportal";
export default class ReactPortal extends React.Component<IReactPortalProps, {}> {
public render(): React.ReactElement<IReactPortalProps, IMyportalProps> {
return (
<div >
<Myportal title="Who we are" body="Our overriding purpose is to dramatically improve the reliability, efficiency........"/>
</div>
);
}
}
MyPortal.tsx
import* as React from "react";
import usePortal from "react-cool-portal";
import "./mystyle.scss";
export interface IMyportalProps {
title: string;
body: string
}
class Myportal extends React.Component
<IMyportalProps> {
constructor(props: IUnderstandStateComponentProps) {
super(props);
}
public render(): React.ReactElement
<IMyportalProps> {
const { Portal, show, hide } = usePortal({ defaultShow: false,containerId:"my-portal-root" });
const handleClickBackdrop = (e: React.MouseEvent) => {
const { id } = e.target as HTMLDivElement;
if (id === "modal") hide();
};
return (
<div className="App">
<button className="btn" onClick={show} type="button">
Who we are
</button>
<button className="btn" onClick={show} type="button">
Our value
</button>
<Portal>
<div
id="modal"
className="modal"
onClick={handleClickBackdrop}
tabIndex={-1}
>
<div
className="modal-dialog"
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
>
<div className="modal-header">
<button
className="modal-close"
onClick={hide}
type="button"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<h1>{this.props.title}</h1>
<h3>.................................................</h3>
{this.props.body}
</div>
</div>
</div>
</Portal>
</div>
);
};
My attempt :)
The idea is, you pass the "description" from the web part down to the dialog.
Please read the link above, how to pass properties in React from parent to the child components (or some basic React tutorial), it is highly recommended if you have chosen to work with SPFx using React.
Please note that an easier approach to create dialogs in SPFx coulod be, to use the Microsoft Fluent UI library, it works great with SPFx, has many tutorials, supports SharePoint themes, etc. In particular, it has the "Dialog" component built-in, among dozens of other useful components and controls.
Anyway, here we go with 'react-cool-portal'. Check out the comments in the code below.
ReactPortal.tsx
import * as React from 'react';
import { IReactPortalProps } from './IReactPortalProps';
import Myportal from "./Myportal";
export default class ReactPortal extends React.Component<IReactPortalProps, {}> {
// pass further the parameter that was received from the web part.
// The "description was passed to "ReactPortal" from WebPart with:
// React.createElement(
// ReactPortal,
// {
// description: this.properties.description
// }
public render(): React.ReactElement<IReactPortalProps> {
return (
<div>
<!-- pass the parameter down to the child component -->
<Myportal description={this.props.description} />
</div>
);
}
}
Myportal.tsx
import* as React from "react";
import usePortal from "react-cool-portal";
import "./mystyle.scss";
// accept properties (added "props" argument)
// and use it below in HTML {props.description}
const Myportal = (props) => {
// const { Portal } = usePortal({ containerId: "my-portal-root" });
const { Portal, show, hide } = usePortal({ defaultShow: false,containerId:"my-portal-root" });
const handleClickBackdrop = (e: React.MouseEvent) => {
const { id } = e.target as HTMLDivElement;
if (id === "modal") hide();
};
return (
<div className="App">
<button className="btn" onClick={show} type="button">
Who we are
</button>
<button className="btn" onClick={show} type="button">
Our value
</button>
<Portal>
<div
id="modal"
className="modal"
onClick={handleClickBackdrop}
tabIndex={-1}
>
<div
className="modal-dialog"
role="dialog"
aria-labelledby="modal-label"
aria-modal="true"
>
<div className="modal-header">
<button
className="modal-close"
onClick={hide}
type="button"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body">
<h1> Who we are</h1>
<!-- use the property value somehow -->
<h3>{props.description}</h3>
</div>
</div>
</div>
</Portal>
</div>
);
};
export default Myportal;
You can achieve this in two ways.
Storing the popup content as part of the page property that is persisted once you edit the page
Storing the popup content in a list and fetching the data whenever the page is render. FYI, haven't fully thought about how to achieve it via this way.
I have updated relevant sections of the code you provided to show how to achieve the expected by storing the popup content as part of the page property.
Added another property to the props to store the popup content
export interface IReactPortalWebPartProps {
description: string;
content?: string;
}
Updated the render method of the component ReactPortalWebPart.ts
public render(): void {
const element: React.ReactElement<any> = React.createElement(
ReactPortal,
{
mode: this.displayMode, //used to determine if to display the text input or not based on the current state of the webpart.
content: this.properties.content, //the contents property of the webpart which is persisted in the page properties.
description: this.properties.description,
updateWebPartContent: this.updateWebPartContentProperty.bind(this) // the function that would be called from the portal component when the content is saved by the user
}
);
private updateWebPartContentProperty(htmlcontent) {
//console.log( htmlcontent )
//console.log( this.properties.content )
this.properties.content = htmlcontent;
this.onPropertyPaneFieldChanged( 'content', this.properties.content, htmlcontent );
//console.log( this.properties.content )
}
Passing the properties from ReactPortal to the Myportal functional component.
<Myportal
_mode={ this.props.mode }
_content={ this.props.content }
_updateWebPartContent={ this.props.updateWebPartContent }
/>
Updated the functional component with the props object, added the hook to save the input from the user and also using the current mode of the page to determine if the text area should be displayed or not.
const Myportal = (props) => {
const [ content, setContent ] = React.useState(props._content);
//abbreviated
return (
<div className="App">
{ props._mode == 2 && <div>
<textarea name="conent" id="conent" cols={30} rows={10} value={ props._content } onChange={ (evt) => { setContent(evt.target.value); } }
</textarea> <br />
<button className="btn" onClick={ () => { props._updateWebPartContent(content) } } type="button" >Save Content</button>
</div> }
<div className="modal-body">
{ props._content }
</div>
I am trying to create a simple pop up using React. In other words, when a user clicks on a button, a simple pop up should appear. However, with my current implementation I am getting a syntax error and I am unsure why. Any help would be much appreciated!
Here is my main file, where my mock_modal is called, e.g. where my popup is called
import PropTypes from 'prop-types';
import React from 'react';
...
class Calendar extends React.Component {
...
mockModalClick () {
}
hoverCustomSlot() {
this.setState({ hoverCustomSlot: !this.state.hoverCustomSlot });
}
render() {
const description = this.state.hoverCustomSlot ?
(<h4 className="custom-instructions">
Click, drag, and release to create your custom event
</h4>)
: null;
const addMockModal = this.props.registrarSupported ? (
<div className="cal-btn-wrapper">
<button
type="submit"
form="form1"
className="save-timetable add-button"
data-for="sis-btn-tooltip"
data-tip
onClick={this.state.seen ? <MockModal toggle={this.togglePop} /> : null}
>
<img src="/static/img/star.png" alt="SIS" style={{ marginTop: '2px' }} />
</button>
<ReactTooltip
id="sis-btn-tooltip"
class="tooltip"
type="dark"
place="bottom"
effect="solid"
>
<span>See my Mock Modal!</span>
</ReactTooltip>
</div>
) : null;
Here is the definition of my pop up
import React, { Component } from "react";
export default class PopUp extends Component {
handleClick = () => {
this.props.toggle();
};
render() {
return (
<div className="modal">
<div className="modal_content">
<span className="close" onClick={this.handleClick}>
×
</span>
<form>
<h3>Register!</h3>
<label>
Name:
<input type="text" name="name" />
</label>
<br />
<input type="submit" />
</form>
</div>
</div>
);
}
}
I have added ellipses and did not include other unnecessary code. The exact error that I am getting is:
ERROR in ./static/js/redux/ui/modals/mock_modal.jsx
web_1 | Module build failed: SyntaxError: Unexpected token (4:14)
export default class PopUp extends Component {
handleClick = () => {
this.props.toggle();
Try adding a const in front of handleClick.
thank you for reading this. I am attempting to learn React by making a dummy website, however I've run into a roadblock.
I want the "display-page" div to only show the Send element initially (which is easy) but when someone clicks one of the 4 options from the content_bar div I want remove the current element and only show the newly clicked element (in this case it is 'Transactions')
I've read about useState and routing but I'm not sure how to implement
Thanks! Please let me know if I didnt give enough details
import React, { Component } from 'react';
import './Data.css';
import Transactions from './Transactions';
import Send from './Send';
class Data extends Component {
constructor(props) {
super(props);
this.state = {
content: <Send />
}
}
transactionpage = () => {
this.setState({content: <Transactions/>});
}
render() {
return(
<div className="content">
<div className="content_bar">
<h5>Send</h5>
<h5 onClick={this.transactionpage}>Transactions</h5>
<h5>Friends</h5>
<h5>Professional</h5>
</div>
<div className="display-page">
{this.state.content}
</div>
</div>
);
}
}
export default Data;
Looking at You can't press an <h5> tag and React code without state feels strange.
You need to learn more to achieve your goal, these are the topics:
JSX expresssion
Conditional rendering
State management
Let me show you my solution, it is one of many ways.
class Data extends Component {
constructor(props) {
super(props);
this.state = {
toDisplay: ''
};
this.changeToDisplay = this.changeToDisplay.bind(this);
}
changeToDisplay(e) {
this.setState({ toDisplay: e.target.textContent.toString() });
}
render() {
return (
<div className="content">
<div className="content_bar">
<button onClick={e => changeToDisplay(e)}>Send</button> <br />
<button onClick={e => changeToDisplay(e)}>Transactions</button> <br />
<button>Friends</button> <br />
<button>Professional</button> <br />
</div>
<div className="display-page">
{this.state.toDisplay === 'Send' ? <Send /> : null}
{this.state.toDisplay === 'Transactions' ? <Transactions /> : null}
</div>
</div>
);
}
}
I am working on the recipe book app and trying to implement the edit entry function.
How it works is to input recipe name (e.g Pasta), followed by ingredients (e.g egg, flour, salt). The ingredients have to be input with commas and will be shown as a list.
Pasta
-Egg
-Flour
i can see that it is somewhat working, because i can see the new entries in the input text (e.g initially was egg,flour,salt -> egg,flour,salt,water) when i tried to edit it again.
However, the extra ingredients (in the above example: water) is not showing up in the list. Do i have to figure a way to re-render the list?
updates:
I think i know where the error might be. There is some issue passing the data and setting the state.
<EditRecipe recipe={this.props.recipe} editRecipe={this.editRecipe.bind(this, this.props.recipe.id, recipe)}/>
App.js
import React, { Component } from 'react';
// import logo from './logo.svg';
import './App.css';
import uuid from 'uuid';
import Modal from 'react-modal';
import RecipeList from './components/RecipeList/RecipeList';
import AddRecipe from './components/AddRecipe/AddRecipe';
class App extends Component {
constructor(props){
super(props);
this.state = {
recipes:[]
};
}
getRecipes(){
this.setState({recipes:[
{
id: uuid.v4(),
food: "pumpkin pie",
ingredients: ["pumpkin puree", "sweetened condensed milk", "eggs", "pumpkin pie spice", "pie crust"]
},
{
id: uuid.v4(),
food: "spaghetti",
ingredients: ["noodles", "tomato sauce", "meatballs"]
},
{
id: uuid.v4(),
food: "onion pie",
ingredients: ["onion", "pie crust"]
},
]});
}
componentWillMount(){
this.getRecipes();
}
handleAddRecipe(recipe){
let recipes = this.state.recipes;
recipes.push(recipe);
this.setState({recipes: recipes});
}
handleDeleteRecipe(id){
let recipes = this.state.recipes;
let index = recipes.findIndex(x => x.id === id);
recipes.splice(index,1);
this.setState({recipes: recipes});
}
handleEditRecipe(id, recipe){
let recipes = this.state.recipes;
let index = recipes.findIndex(x => x.id === id);
recipes.splice(index,1,recipe);
this.setState({recipes: recipes});
}
render() {
return (
<div className="App">
<RecipeList recipes={this.state.recipes} onDelete={this.handleDeleteRecipe.bind(this)} onEdit={this.handleEditRecipe.bind(this)}/>
<AddRecipe addRecipe={this.handleAddRecipe.bind(this)}/>
</div>
);
}
}
export default App;
RecipeList.js
import React, { Component } from 'react';
import Collapsible from 'react-collapsible';
import RecipeItem from '../RecipeItem/RecipeItem'
import './RecipeList.css';
class RecipeList extends Component{
deleteRecipe(id){
this.props.onDelete(id);
}
editRecipe(id, recipe){
this.props.onEdit(id, recipe);
}
render(){
let recipeItem;
if(this.props.recipes){
recipeItem=this.props.recipes.map(recipe => {
return(
<RecipeItem onEdit={this.editRecipe.bind(this)} onDelete={this.deleteRecipe.bind(this)} key={recipe.id} recipe={recipe} />
)
});
}
return(
<div className="recipeList box">
{recipeItem}
</div>
)
}
}
export default RecipeList;
RecipeItem.js
import React, { Component } from 'react';
import Collapsible from 'react-collapsible';
import EditRecipe from '../EditRecipe/EditRecipe';
class RecipeItem extends Component{
deleteRecipe(id){
this.props.onDelete(id);
}
editRecipe(id, recipe){
this.props.onEdit(id, recipe);
}
render(){
let recipe=this.props.recipe
let foodName=recipe.food;
let ingredientItem;
if(recipe.ingredients){
ingredientItem=recipe.ingredients.map(ingredient=>{
return(
<a className="panel-block">
{ingredient}
</a>
)
})
}
return(
<ul>
<li className="Recipe">
<Collapsible trigger={foodName} transitionTime="200" easing="ease-in-out">
<nav className="panel">
<p className="panel-heading">
Ingredients
</p>
{ingredientItem}
<div className="panel-block">
<button className="button is-warning is-outlined" onClick={this.deleteRecipe.bind(this, this.props.recipe.id)}>
Delete
</button>
<EditRecipe recipe={this.props.recipe} editRecipe={this.editRecipe.bind(this, this.props.recipe.id, recipe)}/>
</div>
</nav>
</Collapsible>
</li>
</ul>
);
}
}
export default RecipeItem;
EditRecipe.js
import React, { Component } from 'react';
import RecipeForm from '../RecipeForm/RecipeForm';
// import './EditRecipe.css';
import Modal from 'react-modal';
import uuid from 'uuid';
// import Modal from 'boron/DropModal';
// import './RecipeList.css';
class RecipeEdit extends Component{
constructor(props){
super(props);
this.state = {
revisedRecipe:{
id: this.props.recipe.id,
food: this.props.recipe.food,
ingredients: this.props.recipe.ingredients
},
modalIsOpen: false,
speed: 100
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
openModal(){
this.setState({modalIsOpen: true});
}
closeModal(){
this.setState({modalIsOpen: false});
}
handleSubmit(e){
const revised = this.state.revisedRecipe;
this.props.editRecipe(revised);
e.preventDefault();
}
handleNameChange(e){
this.setState({revisedRecipe:{
food: e.target.value
}
});
}
handleIndChange(e){
this.setState({revisedRecipe:{
ingredients: e.target.value
}
});
}
render(){
const speed = this.state.speed;
let recipe=this.props.recipe;
let foodName=this.state.revisedRecipe.food;
let ingredients=recipe.ingredients;
return(
<div>
<button className="button is-primary" onClick={this.openModal}>Edit Recipe</button>
<Modal
isOpen={this.state.modalIsOpen}
onAfterOpen={this.afterOpenModal}
onRequestClose={this.closeModal}
closeTimeoutMS={speed}
contentLabel="Example Modal"
>
<div className="field">
<h2 className="title is-2">Edit Recipe</h2>
<form>
<label className="label">Recipe</label>
<div className="control">
<input className="input" type="text" placeholder="Recipe Name" ref="recipeName" value={this.state.revisedRecipe.food} onChange={this.handleNameChange.bind(this)}/>
</div>
<div className="field">
<label className="label">Ingredients</label>
<div className="control has-icons-left has-icons-right">
<input className="input" type="text" placeholder="Enter ingredients. (if more than 1 ingredient, separate them with commas)" ref="ingredients" value={this.state.revisedRecipe.ingredients} onChange={this.handleIndChange.bind(this)}/>
<span className="icon is-small is-left">
<i className="fa fa-flask"></i>
</span>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-primary" onClick={this.closeModal}>Edit Recipe</button>
</div>
<div className="control">
<button className="button" onClick={this.closeModal}>Cancel</button>
</div>
</div>
</form>
</div>
</Modal>
</div>
);
}
}
export default RecipeEdit;
I believe you're actually getting an error when trying to re-render after updating a list. The ingredients property in the recipes are an array (as shown in getRecipes()) but you're setting the new state of ingredients (in EditRecipe) as a string: "egg,flour,salt,water" and then trying to render the ingredients as an array: ingredients.map().
When you render an input field with an array <input value={["egg", "flour"]} /> it does show the values separated by comma, but the event.target.value in onChange is actually a string.
In EditRecipe's, handleIndChange could be fixed with:
this.setState({revisedRecipe: {ingredients: e.target.value.split(',')}});
This does have another problem, though in that you are overriding the revisedRecipe completely. So all of the setState calls should be something like:
const recipe = this.state.revisedRecipe;
recipe.ingredients = e.target.value.split(',');
this.setState({revisedRecipe: recipe);
I'm struggling to pose this question in a concise manner. I am having some major performance issues with my app. I have installed the Perf add-on tools for react and can see where the issue is, however I am unsure of the best way to fix it.
I think it will probably have something to do with ReSelect... but need some guidance on where to begin.
I have a component that renders a number of other components. This uses size-me (to calculate the size of the browsing window), and react-grid-layout (to layout each component and permit their positioning to be changed). This is resource intensive, so I can't have this happening unnecessarily.
The user can click on a button to open a modal window (to add or edit the components that are being rendered in the grid).
The issue: When the modal window opens, the underlying component re-renders, causing size-me and react-grid-layout to re-render, which thus causes the modal to "jerkingly" open!
This is the entire state tree:
This is the only part of the state that changes when I open the modal:
The size-me and react-grid-layout stuff is rendering state from the formEngine.form part of the state tree, yet it is being re-rendered when state updates are made to the formEngine.addComponent part of the tree
Here are the performance logs:
As you can see, there are some wasted renders happening AND this will only grow incrementally based on the number of nested layout components the user decides to add to the form...
So to try and prevent this question from becoming too convoluted, let me ask first:
How do I prevent the underlying page from re-rendering when I open the modal?
Why are components that are watching formEngine.form triggered to re-render when fromEngine.addComponent gets modified?
Thank you.
EDIT 1:
I'm not sure if this is relevant, but to answer the comment, I added this code. The AddFormComponent is the Modal that jerks open.
Form.js:
const Form = (props) => (
<div className="form-engine">
<div className="card-block" style={{position: "relative"}}>
{
props.editMode &&
<div className="nula-form-controls">
<AddFormComponent parentId={"root"} />
</div>
}
{
props.form.components.root.childComponentIds.length > 0 ?
<LayoutComponent componentKey={"root"} />
:
<EmptyGridLayout />
}
</div>
</div>
)
LayoutComponent.js:
import React from 'react'
import _ from 'lodash'
import SizeMe from 'react-sizeme'
import { Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import FormComponent from '../containers/FormComponent'
import NestedLayoutComponent from '../containers/LayoutComponent'
import AddFormComponent from '../containers/AddFormComponent'
import LayoutComponentEditor from '../containers/LayoutComponentEditor'
//Setup SizeMe Configuration
let sizeMeConfig = {
monitorWidth: true
}
let sizeMeHOC = SizeMe(sizeMeConfig)
//Wrap ResponsiveReactGridLayout in sizeMeHOC so that it is aware of it's width
var GridLayout = ResponsiveReactGridLayout
GridLayout = sizeMeHOC(GridLayout)
const LayoutComponent = (props) => (
<div>
<GridLayout
cols={props.cols}
className={props.className}
breakpoints={props.breakpoints}
rowHeight={props.rowHeight}
draggableCancel={props.draggableCancel}
layouts={props.layouts}
isDraggable={props.isDraggable}
isResizable={props.isResizable}
onLayoutChange={(currentLayout, allLayouts) => props.handleLayoutChange(props.componentKey, currentLayout, allLayouts)}
width={props.size.width}
>
{
//Map out any child layouts
props.childComponents.map((component) => {
if (component.type === "card") {
return (
<div className={"card card-outline-" + component.color} key={component.key}>
<div className={"card-header card-" + component.color}>
{component.header}
</div>
<div className="card-block" style={{overflowY: "auto", position: "relative"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-card-controls">
<LayoutComponentEditor path={component.key} />
<span className="fa fa-trash" />
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "fieldGroup") {
return (
<div className="card" key={component.key}>
<div className="card-block pl-0 pr-0 pt-2 pb-0" style={{overflowY: "auto"}}>
{
//Hide if editMode={false}
props.editMode &&
<div className="nula-fieldgroup-controls">
<a className="text-warning" title="Edit"><span className="fa fa-pencil" /></a>
<a className="text-danger" title="Remove"><span className="fa fa-trash" /></a>
<AddFormComponent parentId={component.key} />
</div>
}
<NestedLayoutComponent componentKey={component.key} />
</div>
</div>
)
}
else if (component.type === "paragraph") {
return (
<div className="alert alert-success text-font-bold" key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
else {
return (
<div key={component.key}>
{
<FormComponent component={component} editMode={props.editMode} />
}
</div>
)
}
})
}
</GridLayout>
</div>
)
export default SizeMe()(LayoutComponent)
EDIT 2:
AddFormComponent.js -- Component
import React from 'react'
import AddFormComponentDetails from './AddFormComponentDetails'
import Perf from 'react-addons-perf'; // ES6
class AddFormComponent extends React.Component {
constructor(props) {
super(props);
this.localOpenModal = this.localOpenModal.bind(this);
}
localOpenModal() {
console.log("----STARTING PERFORMANCE MONITOR-----")
Perf.start()
this.props.handleOpenModal();
}
componentDidUpdate() {
console.log("-----PERFORMANCE MONITOR STOPPING------")
Perf.stop()
console.log("-----PRINT INCLUSIVE------")
Perf.printInclusive()
console.log("-----PRINT WASTEED------")
Perf.printWasted()
}
render() {
return (
<span>
<a onTouchTap={this.localOpenModal} className="text-success" title="Add Component">
<span className="fa fa-plus" />
</a>
<Modal isOpen={this.props.modalOpen} size={"lgr"} toggle={this.props.handleCloseModal}>
<ModalHeader toggle={this.props.handleCloseModal}>Add Component</ModalHeader>
<ModalBody>
...Removed For Breviety
</ModalBody>
<ModalFooter>
...Removed For Breviety
</ModalFooter>
</Modal>
</span>
)
}
}
export default AddFormComponent
AddFormComponent.js -- Container
import { connect } from 'react-redux'
import {
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
} from '../actions/addFormComponentActions'
import AddFormComponent from '../components/AddFormComponent'
const mapStateToProps = (state) => ({
steps: [
{ icon: 'superpowers', title: 'Select Component', description: 'Select the Component you wish to add', active: state.addComponent.currentStep == 1 },
{ icon: 'info circle', title: 'Enter Details', description: 'Enter details to customize component', active: state.addComponent.currentStep == 2 },
{ icon: 'check', title: 'Add Component', description: 'Add component to form' }
],
currentStep: state.addComponent.currentStep,
modalOpen: state.addComponent.modalOpen,
component: state.addComponent.component,
errors: state.addComponent.errors,
componentType: state.addComponent.componentType
})
export default connect(
mapStateToProps,
{
handleOpenModal,
handleCloseModal,
handleGoBack,
handleComponentPropertyChange,
handleComponentNameChange,
handleComponentTypeChange,
handleSubmit
}
)(AddFormComponent)
addFormComponentReducer.js
import _ from 'lodash'
import {
ADD_FORM_COMPONENT_TOGGLE_MODAL,
ADD_FORM_COMPONENT_CLOSE_MODAL,
ADD_FORM_COMPONENT_GO_BACK,
ADD_FORM_COMPONENT_SUBMIT,
ADD_FORM_COMPONENT_PROPERTY_CHANGE,
ADD_FORM_COMPONENT_PROPERTY_ERROR,
ADD_FORM_COMPONENT_KEY_ERROR,
ADD_FORM_COMPONENT_NAME_CHANGE,
ADD_FORM_COMPONENT_NAME_ERROR,
ADD_FORM_COMPONENT_TYPE_CHANGE,
ADD_FORM_COMPONENT_TYPE_ERROR
} from '../actions/addFormComponentActions'
let initialState = {
currentStep: 1,
modalOpen: false,
component: {
key: '',
label: '',
headingText: '',
text: ''
},
errors: {
key: {
hasError: false,
msg: ''
},
label: {
hasError: false,
msg: ''
},
text: {
hasError: false,
msg: ''
}
}
}
function addFormComponentReducer(state = initialState, action) {
switch (action.type) {
case ADD_FORM_COMPONENT_TOGGLE_MODAL:
return {
...state,
modalOpen: action.payload.isOpen,
currentStep: 1
}
case ADD_FORM_COMPONENT_CLOSE_MODAL:
return initialState;
case ADD_FORM_COMPONENT_GO_BACK:
return {
...state,
currentStep: 1
}
case ADD_FORM_COMPONENT_SUBMIT:
return initialState;
case ADD_FORM_COMPONENT_PROPERTY_CHANGE:
return {
...state,
component: {
...state.component,
[action.payload.key]: action.payload.value
}
}
case ADD_FORM_COMPONENT_PROPERTY_ERROR:
return {
...state,
errors: {
...state.errors,
[action.payload.key]: {
hasError: action.payload.hasError,
msg: action.payload.msg
}
}
}
case ADD_FORM_COMPONENT_TYPE_CHANGE:
return {
...state,
componentType: action.payload.componentType,
currentStep: 2
}
default:
return state
}
}
export default addFormComponentReducer
index.js -- Combine Reducers
import { combineReducers } from 'redux'
//import FormEngine reducers
import formReducer from './formReducer'
//import addFormComponentReducer from './addFormComponentReducer'
import componentEditorReducer from './componentEditorReducer'
const rootFormEngineReducer = combineReducers({
form: formReducer,
//addComponent: addFormComponentReducer,
componentEditor: componentEditorReducer
})
export default rootFormEngineReducer
rootReducer.js
import { combineReducers } from 'redux'
//import reducers
import rootCoreLayoutReducer from '../features/CoreLayout/reducers'
import rootFormEngineReducer from '../features/FormEngine/reducers'
import addComponentReducer from '../features/FormEngine/reducers/addFormComponentReducer'
const rootReducer = combineReducers({
coreLayout: rootCoreLayoutReducer,
formEngine: rootFormEngineReducer,
addComponent: addComponentReducer
})
export default rootReducer
If you are using a pure component any performance optimizations have to be handled manually(using shouldComponentUpdate). Since you are using redux it can handle that for you. But you have to "connect" it to the redux store.
If you choose to use redux connect ensure that the modal visibility is not related to your other properties specifically in your case:
modalOpen is nested in formEngine. When it changes anything else that listens to formEngine will rerender