React Error : is declared but its value is never read. [6133] - javascript

I have created a modal(benficiary Type) and I am trying to call another modal when I am selecting the Person as Beneficiary Type and clicking on the next in the Beneficiary Type modal but while I am trying to implement it, I am failing at below two points:
1. When I am importing the second modal(personModal) it is showing me the message "personModal is declared but its value is never read. [6133]"although I am using that import also and hence it is not getting navigated.
2. On navigation to the personModal after clicking on next button present in the beneficiary type modal, I want to hide the first modal and show only the second modal,without using the react navigator and react routes.Is there a way to do it?
Please help.
For your reference please find the code below:
Benficiary Type Modal
import React, { Component } from 'react';
import Select from 'gssp-common-ui/lib/components/select/select.component';
import Button from 'gssp-common-ui/lib/components/button/button.component';
import personModal from './personModal.component';
const backdropStyle = {
position: 'fixed',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
padding: 50
};
const modalStyle = {
backgroundColor: '#fff',
borderRadius: 5,
maxWidth: 500,
minHeight: 300,
margin: '0 auto',
padding: 30,
position: 'relative'
};
class Modal extends Component {
constructor(props) {
super(props);
this.state = {
dropDownValue: '',
showBeneficiaryModel: false
};
}
handleDropDownChange = (event, value) => {
this.setState({ dropDownValue: value });
}
clickHandlernextBtn = (e) => {
if ((e === 'click') && (this.state.dropDownValue === 'Person')) {
return (
<div>
{console.log('Dropdown value is ', this.state.dropDownValue)}
<personModal />
</div>);
}
};
render() {
if (!this.props.show) {
return null;
}
return (
<div style={backdropStyle}>
<div style={modalStyle}>
<h5><b>{'Add Beneficiary'}</b></h5>
<p>{'All fields are required unless otherwise noted.'}</p>
{/* <form onSubmit={this.handleSubmit}>
<select value={this.state.value} onChange={this.handleChange} >
<b><option value="beneficiary type">{'Beneficiary Type'}</option></b>
<option value="person">{'Person'}</option>
<option value="living trust">{'Living Trust'}</option>
<option value="testamentary trust created in the insured’s will">{'Testamentary Trust created in the Insured’s Will'}</option>
<option value="charity/organization">{'Charity/Organization'}</option>
<option value="estate ">{'Estate '}</option>
</select>
<input type="submit" value="Submit" />
</form> */}
<Select
className="dropdown-class"
title={'Beneficiary Type'}
options={[
{
key: 'Person',
value: 'Person'
},
{
key: 'Living Trust',
value: 'Living Trust'
},
{
key: 'Remove ClasTestamentary Trust created in the Insured’s Will',
`enter code here` value: 'Testamentary Trust created in the Insured’s Will'
enter code here },
{
key: 'Charity/Organization',
value: 'Charity/Organization'
},
{
key: 'Estate',
value: 'Estate'
}
]
}
onEvent={(event, value) => this.handleDropDownChange(event, value)}
/>
<Button
value="NEXT"
className="next_btn"
id="SMDEmployer_schedulepayment_next-button"
onEvent={e => this.clickHandlernextBtn(e)}
/>
</div>
</div>
);
}
}
export default Modal;
personModal.component
import React, { Component } from 'react';
class personModal extends Component {
constructor(props) {
super(props);
this.state = {
dropDownValue: ''
};
}
render() {
return (
<div>
{'Hi PersonModal here'}
</div>
);
}
}
export default personModal;
Note: Button is custom component which is working fine.

I had the same problem. Its simple solution is importing the component with custom names starting with a capital letter.

The personModal component used as <personModal /> will be interpreted as HTML tag as it begins with a lowercase. To use the component rename the component to PersonModal or any other name beginning with an uppercase.

In my setup, I just needed to change the file ending to .tsx, instead of .ts. If you're using JavaScript, the equivalent might be changing a .js file to a .jsx file.

Your file name should be start with capital letter and In latest React version, you don't need to import. And if you are getting 'React' must be in scope when using JSXeslintreact/react-in-jsx-scope then off this in your eslint file like:
"react/react-in-jsx-scope": "off",

Related

In React SharePoint WebPart what are the differences between using 'html-react-parser' & using 'dompurify eslint-plugin-risxss' to securely show HTML

I am trying to build a React.js SharePoint modern web part, which have the following capabilities:-
Inside the Web Part settings page >> there are 2 fields named as "Who We Are" & "Our Value" which allow the user to enter HTML.
The web part will render 2 buttons "Who We Are" & "Our Value" >> and when the user clicks on any button >> a Popup will be shown with the entered HTML code in step-1
Something as follow:-
But to be able to render HTML code as Rich-Text inside my Web Part, i have to use the dangerouslySetInnerHTML attribute inside the .tsx file. as follow:-
import * as React from 'react';
import { useId, useBoolean } from '#fluentui/react-hooks';
import {
getTheme,
mergeStyleSets,
FontWeights,
Modal,
IIconProps,
IStackProps,
} from '#fluentui/react';
import { IconButton, IButtonStyles } from '#fluentui/react/lib/Button';
export const MYModal2 = (myprops) => {
const [isModalOpen, { setTrue: showModal, setFalse: hideModal }] = useBoolean(false);
const [isPopup, setisPopup] = React.useState(true);
const titleId = useId('title');
React.useEffect(() => {
showModal();
}, [isPopup]);
function ExitHandler() {
hideModal();
setisPopup(current => !current)
myprops.handler();
}
return (
<div>
<Modal
titleAriaId={titleId}
isOpen={isModalOpen}
onDismiss={ExitHandler}
isBlocking={true}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
<span id={titleId}>Modal Popup</span>
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="Close popup modal"
onClick={ExitHandler}
/>
</div>
<div className={contentStyles.body}>
<p dangerouslySetInnerHTML={{__html:myprops.OurValue}}>
</p>
</div>
</Modal>
</div>
);
};
const cancelIcon: IIconProps = { iconName: 'Cancel' };
const theme = getTheme();
const contentStyles = mergeStyleSets({
container: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'stretch',
},
header: [
// eslint-disable-next-line deprecation/deprecation
theme.fonts.xLarge,
{
flex: '1 1 auto',
borderTop: '4px solid ${theme.palette.themePrimary}',
color: theme.palette.neutralPrimary,
display: 'flex',
alignItems: 'center',
fontWeight: FontWeights.semibold,
padding: '12px 12px 14px 24px',
},
],
body: {
flex: '4 4 auto',
padding: '0 24px 24px 24px',
overflowY: 'hidden',
selectors: {
p: { margin: '14px 0' },
'p:first-child': { marginTop: 0 },
'p:last-child': { marginBottom: 0 },
},
},
});
const stackProps: Partial<IStackProps> = {
horizontal: true,
tokens: { childrenGap: 40 },
styles: { root: { marginBottom: 20 } },
};
const iconButtonStyles: Partial<IButtonStyles> = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: 'auto',
marginTop: '4px',
marginRight: '2px',
},
rootHovered: {
color: theme.palette.neutralDark,
},
};
And to secure the dangerouslySetInnerHTML, i did the following steps:-
1- Inside my Node.Js CMD >> i run this command inside my project directory:-
npm install dompurify eslint-plugin-risxss
2- Then inside my above .tsx i made the following modifications:-
I added this import import { sanitize } from 'dompurify';
An I replaced this unsafe code <p dangerouslySetInnerHTML={{__html:myprops.OurValue}}></p> with this <div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} />
So I have the following question:-
Now my approach (of using sanitize(myprops.OurValue) will/should securely render HTML as Rich-Text inside the popup since i am using the sanitize function which is part of the dompurify eslint-plugin-risxss. but i read another approach which mentioned that to securely render HTML as Rich-Text inside the popup, we can use the html-react-parser package as follow {parse(myprops.OurValue)}. So what are the differences between using 'html-react-parser' & using 'dompurify eslint-plugin-risxss' to securely render an HTML code as Rich-Text inside the React web part's popup?
Here is my Full web part code:-
inside the MyModalPopupWebPart.ts:-
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 'MyModalPopupWebPartStrings';
import MyModalPopup from './components/MyModalPopup';
import { IMyModalPopupProps } from './components/IMyModalPopupProps';
export interface IMyModalPopupWebPartProps {
description: string;
WhoWeAre: string;
OurValue:string;
}
export default class MyModalPopupWebPart extends BaseClientSideWebPart<IMyModalPopupWebPartProps> {
public render(): void {
const element: React.ReactElement<IMyModalPopupProps> = React.createElement(
MyModalPopup,
{
description: this.properties.description,
WhoWeAre: this.properties.WhoWeAre,
OurValue: this.properties.OurValue
}
);
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('WhoWeAre', {
label: "who We Are",
multiline: true
}),
PropertyPaneTextField('OurValue', {
label: "Our value"
}), PropertyPaneTextField('description', {
label: "Description",
multiline: true
}),
]
}
]
}
]
};
}
}
inside the MyModalPopup.tsx:-
import * as React from 'react';
import { IMyModalPopupProps } from './IMyModalPopupProps';
import { DefaultButton } from '#fluentui/react/lib/Button';
import { MYModal } from './MYModal';
import { MYModal2 } from './MYModal2';
interface IPopupState {
showModal: string;
}
export default class MyModalPopup extends React.Component<IMyModalPopupProps, IPopupState> {
constructor(props: IMyModalPopupProps, state: IPopupState) {
super(props);
this.state = {
showModal: ''
};
this.handler = this.handler.bind(this);
this.Buttonclick = this.Buttonclick.bind(this);
}
handler() {
this.setState({
showModal: ''
})
}
private Buttonclick(e, whichModal) {
e.preventDefault();
this.setState({ showModal: whichModal });
}
public render(): React.ReactElement<IMyModalPopupProps> {
const { showModal } = this.state;
return (
<div>
<DefaultButton onClick={(e) => this.Buttonclick(e, 'our-value')} text="Our Value" />
{ showModal === 'our-value' && <MYModal2 OurValue={this.props.OurValue} myprops={this.state} handler={this.handler} />}
<DefaultButton onClick={(e) => this.Buttonclick(e, 'who-we-are')} text="Who We Are" />
{ showModal === 'who-we-are' && <MYModal WhoWeAre={this.props.WhoWeAre} myprops={this.state} handler={this.handler} />}
</div>
);
}
}
Actually, html-react-parser returns ReactJs object, and its return type is like React.createElement or like type of called JSX.
Using DOMPurify.sanitize will return safe pure HTML elements which those are different to the object that html-react-parser returns. the risxss ESLint plugin will force you to use sanitizing with any kind of sanitize function or library, that I left an answer to your other question to how to Sanitize your string HTML.
Eventually, using sanitizing is better because is the html-react-parser will convert your string HTML to ReactJs object with some tiny changes that would be dangerous because it is possible to have some script of string HTML in the project and it maybe will be harmful it just remove the onclick or onload, etc, from HTML tags but sanitizing will remove all possible harmful tags. also sanitizing will receive configuration, which means you can have your own options for sanitizing.

React Memo Feature gives :- Uncaught Error: Element type is invalid: expected a string but got: object

I have functional component below:-
import React from 'react'
import { Dropdown } from 'semantic-ui-react'
const DropDownMenu= (props)=> {
const options = [
{ key: 'fruits', text: 'fruits', value: 'Fruits' },
{ key: 'vegetables', text: 'vegetables', value: 'Vegetables' },
{ key: 'home-cooked', text: 'home-cooked', value: 'Home-Cooked' },
{ key: 'green-waste', text: 'green-waste', value: 'Green-Waste' },
{ key: 'other', text: 'other', value: 'other' },
];
function onChangeHandler(e) {
console.log(e.target.innerText);
props.getCategoryValue(e.target.innerText);
};
return (
<Dropdown placeholder='Category' fluid selection options={options}
onChange={onChangeHandler} />
);
};
export default React.memo(DropDownMenu);
Above functional component is being rendered in its parent component sellForm.js as below:-
import React,{Component} from 'react'
import { Button, Form} from 'semantic-ui-react'
import AutoCompleteInput from '../GoogleAutocomplete/autoComplete';
import DropDownMenu from '../DropDown/DropDown';
import update from 'react-addons-update';
import './sellForm.css';
import PreviewImages from '../PreviewImage/previewUploadedImages';
import FileInput from '../FileInput/FileInput';
class sellForm extends Component{
constructor(props){
super(props);
//this.imageUpload = React.createRef();
this.state={
postID: '',
title: '',
description:'',
category:'',
price: '',
amount: '',
freshness: '',
contact: '',
location: '',
timestamp: '',
images: []
}
}
getCategoryValue=(category)=>{
this.setState({category: category})
};
getItemLocation=(locationObject)=>{
this.setState({location: locationObject})
};
saveInfo=(e)=>{
this.setState({
[e.target.name]:e.target.value});
};
postButtonClickHandler=()=>{
console.log(this.state)
console.log(typeof (this.state.images[0].file))
// send this info to firebase database
};
handleImageUpload= (file)=>{
console.log('handle image Upload in sell form');
this.setState({
images: update(this.state.images, {$push: [file]})
})
};
handleImageDeletion=(indexOfImage)=>{
console.log('handle image deletion in sell form - index to be deleted is : ' ,indexOfImage);
this.setState((prevState)=>{
return{
// images: prevState.images.splice(indexOfImage,1)
images: update(this.state.images, {$splice: [[indexOfImage,1]]})
}
})
};
shouldComponentUpdate(nextProps,nextState){
console.log('[sellform.js] shouldComponentUpdate');
return true;
}
componentDidMount(){
console.log('[sellform.js] componentDidMount');
}
static getDerivedStateFromProps(props, state){
//when user uploads or deletes images, then props changes
//this lifecycle executes when function gets new props before render()
//only use when component's inner state depends upon props...
console.log('[sellform.js] getDerivedStateFromProps')
return null;
}
componentDidUpdate(prevProps){
console.log('[sellform.js] componentDidUpdate')
}
componentWillUnmount(){
console.log('[sellform.js] componentWillUmMount')
}
render(){
console.log('render of sellForm');
console.log(this.state.images);
let previewImages = (<PreviewImages deleteUploadedImage={this.handleImageDeletion} images={this.state.images}/>)
return(
<Form>
<Form.Field>
<DropDownMenu getCategoryValue={this.getCategoryValue}/>
</Form.Field>
<Form.Field>
{<AutoCompleteInput
onChange={()=>{}}
onPlaceSelected={this.getItemLocation}/>}
</Form.Field>
<Form.Field>
<input
placeholder='What are you selling ?'
name="title"
onChange={this.saveInfo}/>
</Form.Field>
<Form.Field>
<input
placeholder='Price'
name="price"
onChange={this.saveInfo} />
</Form.Field>
<Form.Field>
<FileInput appendImageToArray={this.handleImageUpload}/>
</Form.Field>
<Form.Field>
<Button
type='submit'
onClick={this.postButtonClickHandler}>Post
</Button>
</Form.Field>
<Form.Field>
<div className='previewImageContainer'>
{previewImages}
</div>
</Form.Field>
</Form>
)
}
}
export default sellForm
when sellFom renders it gives following error:-
Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of sellForm.
at invariant (react-dom.development.js:57)
Any ideas react community ??
I solved this issue by updating both react and react-dom to 16.6.0.
Hey I think problem is due to naming od sellForm. Ax far as know, React accepts CamelCase Name for classes. Take this example for now:
function Example() {
// Declare a new state variable, which we'll call "count"
return (
<div>
<p>Tes</p>
</div>
);
}
const MemoizedExample = React.memo(Example)
function exampleParent() {
// Declare a new state variable, which we'll call "count"
return (
<div>
<p>Parent</p>
<MemoizedExample />
</div>
);
}
ReactDOM.render(<exampleParent />, document.getElementById("root"))
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In above I have made name as smallcase for class, see the rendering doesn't happen.
If you change the name of component from exampleComponent to ExampleComponent It will work. Similiarly for your problem change your class name from sellFrom to SellForm :). Here is the working one with component name camelcase:
function Example() {
// Declare a new state variable, which we'll call "count"
return (
<div>
<p>Tes</p>
</div>
);
}
const MemoizedExample = React.memo(Example)
function ExampleParent() {
// Declare a new state variable, which we'll call "count"
return (
<div>
<p>Parent</p>
<MemoizedExample />
</div>
);
}
ReactDOM.render(<ExampleParent />, document.getElementById("root"))
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

pass props to dynamic child component

I am trying to create a multiple step form. I have created the form where in each step, corresponding form is rendered dynamically. But I have no idea on how should I pass props to those component so that when returning back, the state gets preserved. I have created a sandbox of it in codesandbox and here it is
https://codesandbox.io/s/8xzm2mxol2
The rendering of form is done the following way
{this.props.steps[this.state.componentState].component}
If the component is rendered as below which is static way, the code would be something like this but I want the dynamic way
if(this.state.componentState === 1) {
<Form1 props={props} />
}
The code is
import React from 'react';
import './fullscreenForm.css';
class MultipleForm extends React.PureComponent {
constructor(props) {
super(props);
this.hidden = {
display: "none"
};
this.state = {
email: 'steve#apple.com',
fname: 'Steve',
lname: 'Jobs',
open: true,
step: 1,
showPreviousBtn: false,
showNextBtn: true,
componentState: 0,
navState: this.getNavStates(0, this.props.steps.length)
};
}
getNavStates(indx, length) {
let styles = [];
for (let i = 0; i < length; i++) {
if (i < indx) {
styles.push("done");
} else if (i === indx) {
styles.push("doing");
} else {
styles.push("todo");
}
}
return { current: indx, styles: styles };
}
checkNavState(currentStep) {
if (currentStep > 0 && currentStep < this.props.steps.length) {
this.setState({
showPreviousBtn: true,
showNextBtn: true
});
} else if (currentStep === 0) {
this.setState({
showPreviousBtn: false,
showNextBtn: true
});
} else {
this.setState({
showPreviousBtn: true,
showNextBtn: false
});
}
}
setNavState(next) {
this.setState({
navState: this.getNavStates(next, this.props.steps.length)
});
if (next < this.props.steps.length) {
this.setState({ componentState: next });
}
this.checkNavState(next);
}
next = () => {
this.setNavState(this.state.componentState + 1);
};
previous = () => {
if (this.state.componentState > 0) {
this.setNavState(this.state.componentState - 1);
}
};
render() {
return (
<div className="parent-container">
<div className="form-block">
{this.props.steps[this.state.componentState].component}
</div>
<div
className="actions"
style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'flex-end'}}
>
<button
style={this.state.showPreviousBtn ? {} : this.hidden}
className="btn-prev"
onClick={this.previous}
>
Back
</button>
<button
style={this.state.showNextBtn ? {} : this.hidden}
className="btn-next"
onClick={this.next}
>
Continue
</button>
</div>
</div>
);
}
}
export default MultipleForm;
I wanted it in best practice way.
You need to save the values of your form for all the step inputs. Now since on every step you are changing the form component, so you cannot put those values in corresponding form component. Therefore you have to put those values in the parent container (i.e. MultipleForm). Now as you are maintaining a state of values of your child component in parent container, therefore you will have to put some kind of mechanism so that whenever there is any change in input in child component, it should update the corresponding state in parent container. For that you can pass a change handler function to you child component. So your form component should look something like this
<div className="fullscreen-form">
<div className="custom-field">
<label className="custom-label fs-anim-upper" for="email">
What's your email address?
</label>
<input
className="fs-anim-lower"
id="email"
name="email"
type="email"
onChange={this.props.handleChange} // Whenver the input changes then call the parent container's handleChange function so that it can update it's state accordingly
value={this.props.value} // Updated value passed from parent container
placeholder="steve#apple.com"
/>
</div>
</div>
And you will render your form something like this
<Form1 handleChange={this.handleChange} value={this.state.email} />
Here's a working solution of your code:: Code

How to pass one state value to another state on initialization in react js

I am trying to pass one state value that is imagesArray to another state that is tabData, but it is coming as undefined, PFB the code, please tell what i am doing wrong here?
constructor(props) {
super(props);
this.state = {
imagesArray: [
{
default: '/images/volImage1.png',
active: 'images/volImage1.png'
},
{
default: '/images/volImage2.png',
active: 'images/volImage2-Active.png'
},
{
default: '/images/volImage3.png',
active: 'images/volImage3.png'
},
{
default: '/images/volImage4.png',
active: 'images/volImage4.png'
},
{
default: '/images/volImage5678.png',
active: 'images/volImage5678.png'
},
],
tabData: [
{
title: 'Knowledge and experience',
content: <VolunteerTabContent1 imagesArray={this.state.imagesArray} />
//Here I am passing above imagesArray state, and this is coming as undefined and throwing error
},
{
title: 'Practical and hands on',
content: 'Tab 2 Content'
},
{
title: 'Management and leadership',
content: 'Tab 3 Content'
},
]
}
}
You cannot use this.state when setting the state itself. This won't work at all.
In your case, if imagesArray is not going to be changed during the execution and it's only some data you need, maybe you don't need to set it as part of the component's state.
You could define imagesArray as a constant outside the class or something similar, and just reference it when setting the tabData.
EDIT:
Even more. If tabData is just data you will need afterwards but it's not going to change, you don't need to set that as state either.
EDIT 2:
If this two arrays really need to be in the state, a way to achieve the desired results would be to define only the component name in the 'content' property of each tabData item, and then use that to instantiate it in the render method:
tabData: [
{
title: 'Knowledge and experience',
content: VolunteerTabContent1
},
...
and then in the render method you can do:
// Let's suppose you want the first one as an example. Do this as you need.
const item = this.state.tabData[0];
render() {
<item.content imagesArray={this.state.imagesArray} />
}
This way you'll correctly get the imagesArray form the state. JSX will get item.content's value (the VolunteerTabContent1 component in this case) and render it.
I will show in functional component it will useful for someone.
import "./styles.css";
import image1 from '../image/man.png';
import image2 from '../image/woman.png';
import React,{useState} from 'react';`enter code here`
import Avatar from '#mui/material/Avatar';
export default function App() {
const [choose,setChoose] =useState("")
const [avatar,setAvatar] = useState(image);
return (
<div className="App">
<Avatar
alt="D S"
src={avatar}
sx={{ width: 44, height: 44 }}
/>
<div>
<Avatar className='Avatars' onClick={()=> setChoose(image1)} alt="Guest"
src={image1} sx={{ width: 30, height: 30 }}/>
<label>Guest</label>
</div>
<div>
<Avatar className='Avatars' onClick={()=> setChoose(image2)} alt="Other"
src={image2} sx={{ width: 30, height: 30 }}/>
<label>Other</label>
</div>
<div>
<button className="avatar_btn1" onClick={()=>setAvatar(choose)} >OK</button>
</div>
</div>
);
}
from my code first you can choose a avatar it will not change in frontend when you click ok button it will show avatar changed one.

Rendered HTML not in sync with React component

I'm currently creating a custom React component in Meteor for adding images to a list (and later uploading them). However when I try to delete images from the list, always the last element is removed from the GUI. Initially I thought this was just a simple case of using the wrong index for deletion, but it turned out to be more than that.
This is what my ImageList component currently looks like:
import React from 'react';
import Dropzone from 'react-dropzone';
import cloneDeep from 'lodash.clonedeep';
import { ImageItem } from './image-item.js';
export class ImagesList extends React.Component {
constructor(props) {
super(props);
this.values = this.props.images || [];
this.onDrop = this.onDrop.bind(this);
this.addImages = this.addImages.bind(this);
this.deleteImage = this.deleteImage.bind(this);
this.imageChanged = this.imageChanged.bind(this);
}
onDrop(files) {
this.addImages(files);
}
onDropRejected() {
alert('Invalid file type');
}
addImages(files) {
files.forEach(file => {
this.values.push({
title: '',
description: '',
url: file.preview,
file,
});
});
this.forceUpdate();
}
deleteImage(index) {
console.log('index to delete', index);
console.log('images pre-delete', cloneDeep(this.values)); // deep-copy because logging is async
this.values.splice(index, 1);
console.log('images post-delete', cloneDeep(this.values)); // deep-copy because logging is async
this.forceUpdate();
}
imageChanged(index, image) {
this.values[index] = image;
this.forceUpdate();
}
render() {
console.log('--------RENDER--------');
return (
<div className="image-list">
<div className="list-group">
{this.values.length === 0 ?
<div className="list-group-item">
No images
</div>
:
this.values.map((image, index) => {
console.log('rendering image', image);
return (
<ImageItem
key={index}
image={image}
onDelete={() => { this.deleteImage(index); }}
onChange={(item) => { this.imageChanged(index, item); }}
deletable={true}
/>
);
})
}
</div>
<Dropzone
multiple={true}
onDrop={this.onDrop}
onDropRejected={this.onDropRejected}
className="dropzone"
activeClassName="dropzone-accept"
rejectStyle={this.rejectStyle}
accept={'image/*'}
>
<span>Drop files here</span>
</Dropzone>
</div>
);
}
}
The ImagesList component can be initialized with some values (for the sake of debugging), which it uses during rendering. For example:
<ImagesList images={[
{ title: 'Image 1', description: 'Image 1 description', url: 'http://cssdeck.com/uploads/media/items/3/3yiC6Yq.jpg' },
{ title: 'Image 2', description: 'Image 2 description', url: 'http://cssdeck.com/uploads/media/items/4/40Ly3VB.jpg' },
{ title: 'Image 3', description: 'Image 3 description', url: 'http://cssdeck.com/uploads/media/items/0/00kih8g.jpg' },
]}/>
ImagesList renders an ImageItem component for each image. This is what this component looks like:
import React from 'react';
import { RIEInput, RIETextArea } from 'riek';
export class ImageItem extends React.Component {
constructor(props) {
super(props);
this.placeholder = {
title: 'Title',
description: 'Description',
};
this.value = this.props.image;
}
render() {
return (
<div className="list-group-item">
<div className="text-content">
<h4>
<RIEInput
className="description"
value={this.value.title.length <= 0 ?
this.placeholder.title : this.value.title}
change={(item) => {
this.value.title = item.value;
this.props.onChange(this.value);
}}
validate={(value) => value.length >= 1}
classEditing="form-control"
propName="value"
/>
</h4>
<span>
<RIETextArea
className="description"
value={this.value.description.length <= 0 ?
this.placeholder.description : this.value.description}
change={(item) => {
this.value.description = item.value;
this.props.onChange(this.value);
}}
validate={(value) => value.length >= 1}
classEditing="form-control"
propName="value"
rows="2"
/>
</span>
</div>
<img className="thumb img-responsive"
style={{width: '20%' }}
src={this.value.url}
alt="Image"
data-action="zoom"
/>
{this.props.deletable ?
<div className="delete-btn">
<span onClick={this.props.onDelete}>
×
</span>
</div>
:
undefined }
</div>
);
}
}
Let's say I have three images, image A, B and C, and I want to delete image B. After pressing the delete button, image C will disappear from the GUI instead.
Inside the deleteImage() function of ImagesList, I am logging the index that is to be deleted and also log the values before and after the deletion. The index that is logged is correct, in this case that is index 1. Before the deletion the values are images A, B and C. After deletion the values are images A and C, as they should be.
I decided to do some logging inside the render() function of ImagesList as well. Unfortunately this also logs the correct values A and C, but A and B are actually rendered.
I have also tried to use the React state for this component instead of storing it in a local variable in conjunction with forceUpdate().
Another thing I have tried is to use the React Developer Tools plugin for Chrome. The Devtools also show the correct values, but the GUI still does not, as shown in this screenshot.
I'm currently out of ideas on what to try, any help would be appreciated!
Using the snippets I provided, you should be able to create a Meteor project and reproduce this bug.
With MasterAM's suggestion I managed to find two different solutions.
A.) Using componentWillUpdate()
The this.value variable is set only once namely in the constructor of the ImageItem component. To ensure that changes are properly delegated, you have to update this.value inside the componentWillUpdate() function. Something like:
componentWillUpdate(nextProps, nextState) {
this.value = nextProps.image;
}
B.) Using the property directly
This is definitely the more proper solution. Here we get rid of the local variable this.value inside the constructor of the ImageItem component.
Inside the render() function you replace this.value with this.props.image. Now without having to use the componentWillUpdate() function, everything works as expected.

Categories