Why doesn't this React component re-render when its prop changes? - javascript

I've created a cut-down version of a re-render issue which I'm having with an application I am working on.
In reality DisplayElement1 and DisplayElement2 are two complex components.
DisplayElement2 here is iterating through a simple array of numbers (supplied via its prop numbers) and displaying them.
Problem : When the array behind the numbers prop gets updated in the main component App (in this case by clicking on the Add Number to Array button I would expect DisplayElement2 to re-render with the updated array but it doesn't, why not ??
If I click Show Display 1 and then click back on Show Display 2 the updated array renders.
App.js
import React, { useState, useMemo } from "react";
import "./styles.css";
import DisplayComponent1 from "./DisplayComponent1";
import DisplayComponent2 from "./DisplayComponent2";
import { Button } from "#material-ui/core";
export default function App() {
const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);
////////////////////////////////////////////////
const component1 = useMemo(() => {
return <DisplayComponent1 />;
}, []);
const component2 = useMemo(() => {
return (
<DisplayComponent2
style={{ background: "red" }}
numbers={numbersToDisplay}
/>
);
}, [numbersToDisplay]);
////////////////////////////////////////////////
const [currentDisplayComponent, setCurrentDisplayComponent] = useState(
component2
);
return (
<div className="App">
<Button
variant="contained"
color="secondary"
onClick={() => setCurrentDisplayComponent(component1)}
>
Show Display 1
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setCurrentDisplayComponent(component2)}
>
Show Display 2
</Button>
<Button
variant="contained"
style={{ marginLeft: 50 }}
onClick={() => {
let tempArray = Array.from(numbersToDisplay);
tempArray.push(4);
setNumbersToDisplay(tempArray);
}}
>
Add number to array
</Button>
{currentDisplayComponent}
</div>
);
}
DisplayElement1.js and DisplayElement2.js
import React from "react";
import {Paper} from "#material-ui/core";
export default function DisplayComponent1(props) {
return (
<Paper>
<p>This is DisplayComponent1</p>
</Paper>
);
}
import React from "react";
import { Paper } from "#material-ui/core";
export default function DisplayComponent2(props) {
return (
<Paper>
<p>This is DisplayComponent2</p>
{props.numbers.map((currNumber, currIndex) => {
return <div key={currIndex}>{currNumber}</div>;
})}
</Paper>
);
}

The reason your component doens't re-render with updated props is because you have a previous instance of your component stored in the currentDisplayComponent state which is what you use to render
A hacky workaround with your current code would be to make use of useEffect and update the component instance that is active
However the best solution in this scenarios is to take out the component instances outside of the state and render these based on a selected component string state.
To prevent unnecessary updates you can make use of React.memo
export default React.memo(function DisplayComponent2(props) {
return (
<Paper>
<p>This is DisplayComponent2</p>
{props.numbers.map((currNumber, index) => {
return <div key={index}>{currNumber}</div>;
})}
</Paper>
);
});
App.js
export default function App() {
const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);
const [currentDisplayComponent, setCurrentDisplayComponent] = useState(
"component1"
);
const getCurrentComponent = currentDisplayComponent => {
switch (currentDisplayComponent) {
case "component1":
return <DisplayComponent1 />;
case "component2":
return (
<DisplayComponent2
style={{ background: "red" }}
numbers={numbersToDisplay}
/>
);
default:
return null;
}
};
return (
<div className="App">
<Button
variant="contained"
color="secondary"
onClick={() => setCurrentDisplayComponent("component1")}
>
Show Display 1
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setCurrentDisplayComponent("component2")}
>
Show Display 2
</Button>
<Button
variant="contained"
style={{ marginLeft: 50 }}
onClick={() => {
let tempArray = Array.from(numbersToDisplay);
tempArray.push(4);
setNumbersToDisplay(tempArray);
}}
>
Add number to array
</Button>
{getCurrentComponent(currentDisplayComponent)}
</div>
);
}
Working demo

Consider something like this for your App.js, where the display states are enumerated and we've removed the useMemo
import React, { useState } from "react";
import "./styles.css";
import DisplayComponent1 from "./DisplayComponent1";
import DisplayComponent2 from "./DisplayComponent2";
import { Button } from "#material-ui/core";
const DisplayStatEnum = {COMPONENT1: 0, COMPONENT2: 1};
export default function App() {
const [numbersToDisplay, setNumbersToDisplay] = useState([1, 2, 3]);
////////////////////////////////////////////////
const component1 = <DisplayComponent1 />;
const component2 = <DisplayComponent2
style={{ background: "red" }}
numbers={numbersToDisplay}
/>;
////////////////////////////////////////////////
const [currentDisplayComponent, setCurrentDisplayComponent]
= useState(DisplayStatEnum.COMPONENT2);
const componentSelected =
currentDisplayComponent === DisplayStatEnum.COMPONENT1
? component1
: component2;
return (
<div className="App">
<div>
<Button
variant="contained"
color="secondary"
onClick={() => setCurrentDisplayComponent(DisplayStatEnum.COMPONENT1)}
>
Show Display 1
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setCurrentDisplayComponent(DisplayStatEnum.COMPONENT2)}
>
Show Display 2
</Button>
</div>
<Button
variant="contained"
style={{ marginLeft: 50 }}
onClick={() => {
let tempArray = Array.from(numbersToDisplay);
tempArray.push(4);
setNumbersToDisplay(tempArray);
}}
>
Add number to array
</Button>
{componentSelected}
</div>
);
}

Related

Date and ToggleButton retrieval on a stepper in react js

I am working on a small table reservation application for a restaurant.
For this I used e Stepper from Material UI.
On the first step : you have to get the date and the available table and continue the reservation.
The problem is that I could not get the data from the selected table and date time, and I could not move to the second step.
Here is the code of the checkout and the github link of the project.
Anybody have a clearer idea about the solution?
Thanks in advance,
import React, { useState } from 'react';
import {
Stepper,
Step,
StepLabel,
Button,
Typography,
CircularProgress
} from '#material-ui/core';
import { Formik, Form } from 'formik';
import AddressForm from './Forms/AddressForm';
//import PaymentForm from './Forms/PaymentForm';
import ReservationForm from './Forms/ReservationForm';
import Summary from './Forms/Summary';
import CheckoutSuccess from './CheckoutSuccess';
import validationSchema from './FormModel/validationSchema';
import checkoutFormModel from './FormModel/checkoutFormModel';
import formInitialValues from './FormModel/formInitialValues';
import useStyles from './styles';
const steps = ['Book your table', 'Personal information', 'Summary'];
const { formId, formField } = checkoutFormModel;
function _renderStepContent(step) {
switch (step) {
case 0:
return <ReservationForm formField={formField} />;
case 1:
return <AddressForm formField={formField} />;
case 2:
return <Summary />;
default:
return <div>Not Found</div>;
}
}
export default function CheckoutPage() {
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const currentValidationSchema = validationSchema[activeStep];
const isLastStep = activeStep === steps.length - 1;
function _sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function _submitForm(values, actions) {
await _sleep(1000);
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
setActiveStep(activeStep + 1);
}
function _handleSubmit(values, actions) {
if (isLastStep) {
_submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
function _handleBack() {
setActiveStep(activeStep - 1);
}
return (
<React.Fragment>
<Typography component="h1" variant="h4" align="center">
Checkout
</Typography>
<Stepper activeStep={activeStep} className={classes.stepper}>
{steps.map(label => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
<React.Fragment>
{activeStep === steps.length ? (
<CheckoutSuccess />
) : (
<Formik
initialValues={formInitialValues}
validationSchema={currentValidationSchema}
onSubmit={_handleSubmit}
>
{({ isSubmitting }) => (
<Form id={formId}>
{_renderStepContent(activeStep)}
<div className={classes.buttons}>
{activeStep !== 0 && (
<Button onClick={_handleBack} className={classes.button}>
Back
</Button>
)}
<div className={classes.wrapper}>
<Button
disabled={isSubmitting}
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
{isLastStep ? 'Place order' : 'Next'}
</Button>
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</div>
</div>
</Form>
)}
</Formik>
)}
</React.Fragment>
</React.Fragment>
);
}
i saw your code and the problem is in the validation schema , you are using the schema of the next step in this step ,
and you should pass name for the input ,
// {...props} => line 43, DatePickerField.jsx file
you made it then you disable it , so just return it back
good luck

How to sync state in react.js?

I am having difficulty updating my state and my component. After a button is pressed and I change the value of one of the props of the popup component. The value of the props is not updated. I believe that this is one of the side effects of using the setstate. I did some research and saw that there is a way to solve this problem using the useeffect hook but I am unable to receive the result. Here is my code below:
My goal is to get from the form having the prop of Data0 to have a prop of Data1, but the prop does not seem to be updating at all.
I am simulating clicking multiple objects and the result is an update in the value of fromData. Thus, app.js is my parent component. The child component is the popup, whose value should change to Bob and an actual date instead of just string values of the original name and original date.
import React, { useState, useEffect } from 'react';
import './App.css';
import FormDialog from './component/popup'
import Button from '#material-ui/core/Button';
function App() {
const Data0 = { name:'original name', date:'original date' }
const Data1 = { name:'Bob', date:'1939' }
const [formStatus, setformStatus] = React.useState(false);
const [formdata2, setformData2] = useState(Data0)
const [tempform, settempform] = useState(<FormDialog formStatus = {formStatus} handelForm={() => handelForm()} Data0={Data0}/>)
const handelForm = () => {
const tempform = <FormDialog formStatus = {formStatus} handelForm={() => handelForm()} Data0={Data1}/>
settempform(tempform);
};
useEffect(() => {
const tempform = <FormDialog formStatus = {formStatus} handelForm={() => handelForm()} Data0={Data1}/>
settempform(tempform);
setformStatus(!formStatus);
console.log('formdata2 EFFECT', formdata2)
settempform(tempform);
setformStatus(!formStatus);
setformStatus(!formStatus);
}, [formdata2]
);
return (
<div className="App">
<h1 align="center">React-App</h1>
<h4 align='center'>Render Custom Component in Material Table</h4>
<Button variant="outlined" color="primary" onClick={() => handelForm()}>
Vendor row
</Button>
{tempform}
{formdata2.billingVendor}
</div>
);
}
export default App;
export default function FormDialog (props) {
let [Data, setData] = React.useState(props.Data0);
return (
<React.Fragment>
<Dialog
maxWidth='lg'
open={props.formStatus}
aria-labelledby="max-width-dialog-title"
disableBackdropClick= {true}
disableEscapeKeyDown={true}
>
<DialogTitle className={classes.title}>{Data.name}</DialogTitle>
<Divider />
<DialogContent>
<DialogContentText>
Please be cautious when updating the fields below.
</DialogContentText>
<form noValidate>
<FormControl className={classes.formControl} fullWidth= {true}>
<div className={classes.root}>
<TextField
fullWidth
label='Date'
style={{ margin: 8 }}
disabled
value={Data.name}
variant="outlined"
/>
<br/>
<TextField
fullWidth
label='Date'
style={{ margin: 8 }}
disabled
value={Data.name}
variant="outlined"
/>
<br/>
<TextField
fullWidth
style={{ margin: 8 }}
disabled
value={Data.date}
variant="outlined"
/>
<br/>
<TextField
fullWidth
style={{ margin: 8 }}
disabled
value={Data.date}
variant="outlined"
/>
<br/>
</div>
</FormControl>
</form>
</DialogContent>
<DialogActions>
<Button onClick={() => props.handelForm()} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}```
I think the process you are following is not a good one. You shouldn't store a react component in the state rather you should dynamically load the component or pass what prop you need.
import React, { useState, useEffect } from 'react';
import './App.css';
import FormDialog from './component/popup'
import Button from '#material-ui/core/Button';
function App() {
const Data0 = { name:'original name', date:'original date' }
const Data1 = { name:'Bob', date:'1939' }
const [formStatus, setformStatus] = React.useState(false);
const [formdata2, setformData2] = useState(Data0)
const [formData, setFormData] = useState(Data0)
const handelForm = () => {
// here change the state however you want
setFormData(Data0);
};
return (
<div className="App">
<h1 align="center">React-App</h1>
<h4 align='center'>Render Custom Component in Material Table</h4>
<Button variant="outlined" color="primary" onClick={() => handelForm()}>
Vendor row
</Button>
<FormDialog formStatus = {formStatus} handelForm={() => handelForm()} Data0={formData}/>
</div>
);
}
export default App;
In the FormDialog add the useEffect to perform the change
useEffect(() => {
setData(props.Data0)
}, [props.Data0])
This is update the state with the changes
You are creating a new state based on the value of the props in your child component, which is independent to the state in the parent component. So a change in the child cannot be passed back to the parent.
To fix it,
create the state in your parent component by
const [Data0, setData0] = useState({ name:'original name', date:'original date' })
pass the setState function to change the value in the parent component to your children by
const tempform = <FormDialog formStatus = {formStatus} handelForm={() => handelForm()} Data={Data1} setData={setData0}/>
change the value in your child component accordingly
let {Data, setData} = props;
Then the call of setData should be calling the one in your parent component and it should be able to update the value accordingly.

My modal doesn't show when I click a button

Here I have my modal component. I am making an app that I want a button to open this modal that I use in multiple places like opening a preview or deleting options.
import React from 'react';
import ReactDOM from 'react-dom';
import { CSSTransition } from 'react-transition-group';
import Backdrop from '../Backdrop/Backdrop';
import '../Modal/Modal.css';
const ModalOverlay = (props) => {
const content = (
<div className={`modal ${props.className}`} style={props.style}>
<header className={`modal__header ${props.headerClass}`}>
<h2>{props.header}</h2>
</header>
<form
onSubmit={
props.onSubmit ? props.onSubmit : (event) => event.preventDefault()
}
>
<div className={`modal__content ${props.contentClass}`}>
{props.children}
</div>
<footer className={`modal__footer ${props.footerClass}`}>
{props.footer}
</footer>
</form>
</div>
);
return ReactDOM.createPortal(content, document.getElementById('modal-hook'));
};
const Modal = (props) => {
return (
<React.Fragment>
{props.show && <Backdrop onClick={props.onCancel} />}
<CSSTransition
in={props.show}
mountOnEnter
unmountOnExit
timeout={200}
classNames="modal"
>
<ModalOverlay {...props} />
</CSSTransition>
</React.Fragment>
);
};
export default Modal;
And here I use this modal for showing up deleting options.
const DocumentItem = (props) => {
const [showConfirmModal, setShowConfirmModal] = useState(false);
const showDeleteWarningHandler = () => {
setShowConfirmModal(true);
};
const calcelDeleteHandler = () => {
setShowConfirmModal(false);
};
const confirmDeleteHandler = () => {
setShowConfirmModal(false);
console.log('Delete!');
};
return (
<React.Fragment>
<Modal
show={showConfirmModal}
onCancel={calcelDeleteHandler}
header="Are you sure?"
footerClass="document-item__modal-actions"
footer={
<React.Fragment>
<Button inverse onClick={calcelDeleteHandler}>
CANCEL
</Button>
<Button danger onClick={confirmDeleteHandler}>
DELETE
</Button>
</React.Fragment>
}
>
<p>
Do you want to proceed and delete this document? Please note that it
can't be undone thereafter.
</p>
</Modal>
</React.Fragment>
);
};
I don't understand why my screen goes all black, transparent but my modal doesn't show.
How can I fix this problem?

How to avoid unexpected rendering while using React Context?

I have two functional component under my provider,
SubApp1 and SubApp2 and here when I am increasing counter1 in SubApp1 the SubApp2 also is rendering, even when it is not need to be re-rendered.
And when I am increasing counter2 in SubApp2 the SubApp1 also is rendering.
I know this happens regally, but How can avoid this situation ?
App.js:
import React, {useContext, useState, memo} from "react";
import "./styles.css";
export const MainContext = React.createContext();
export const MainProvider = ({children})=> {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
return (
<MainContext.Provider value={{
counter1, setCounter1,
counter2, setCounter2,
}}>
{children}
</MainContext.Provider>
);
}
export const SubApp1 = memo(()=> {
const {counter1, setCounter1} = useContext(MainContext);
console.log('Counter 1: ', counter1);
return (
<div className="App">
<button onClick={()=> {
setCounter1(counter1+1);
}}>
Increase Count 1
</button>
</div>
);
});
export const SubApp2 = memo(()=> {
const {counter2, setCounter2} = useContext(MainContext);
console.log('counter2: ', counter2);
return (
<div className="App">
<button onClick={()=> {
setCounter2(counter2+1);
}}>
Increase Count 2
</button>
</div>
);
});
export default function App ({navigation}){
console.log('App Is rendering...');
return (
<div className="App">
<button onClick={()=> {
navigation.navigate('SubApp1');
}}>
navigate to SubApp1
</button>
<button onClick={()=> {
navigation.navigate('SubApp2');
}}>
navigate to SubApp2
</button>
</div>
);
}
index.js:
import React from "react";
import ReactDOM from "react-dom";
import App, {MainProvider} from "./App";
const MainApp = ()=> (
<MainProvider>
<App />
</MainProvider>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<MainApp />, rootElement);
You should pass the counter to the SubApps as props. Then memo will take care that only the component with changing props will be rerendered.
Something like this:
export const Wrapper1 = ()=> {
const {counter1, setCounter1} = useContext(MainContext);
return (
<SubApp1 {...{counter1, setCounter1}} />
);
};
export const SubApp1 = memo(({counter1, setCounter1})=> {
console.log('Counter 1: ', counter1);
return (
<div className="App">
<button onClick={()=> {
setCounter1(counter1+1);
}}>
Increase Count 1
</button>
</div>
);
});
export const SubApp2 = memo(({counter2, setCounter2})=> {
console.log('counter2: ', counter2);
return (
<div className="App">
<button onClick={()=> {
setCounter2(counter2+1);
}}>
Increase Count 2
</button>
</div>
);
});
export default function App (){
const {counter2, setCounter2} = useContext(MainContext);
console.log('App Is rendering...');
return (
<div className="App">
<Wrapper1/>
<SubApp2 {...{counter2, setCounter2}} />
</div>
);
}
Codesandbox link is not right...
I follow the tip of Peter Ambruzs, but i have a problem if i pass counter1 as a param. The component keep rerendering.
But, if i pass just setCounter1 function, its works fine.
Below, my example using typescript.
const Campaigns = (): JSX.Element => {
const { setAlert } = useContext(AlertContext);
return <InnerCampaign {...{ setAlert }} />;
};
const InnerCampaign = memo(
({ setAlert }: any): JSX.Element => {...},)
export default Campaigns;

How do I delete a card component using id from a data object?

I have built a Trello clone using ReactJS, where I have 4 columns called TODO, DOING, DONE and REJECTED, where I can add a card to any column.
In a file I am trying to map over card component and rendering properties from defined dummy data.
What I want to do?
I want to delete a specific card when it is clicked using the card id in some way.
What is the problem?
I do not understand how do I delete an entire card object when it matches a certain id.
My TaskboardList.js component :
import React from "react";
import TaskboardCard from "./TaskboardCard";
import TaskboardActionButton from "./TaskboardActionButton";
import { Droppable } from "react-beautiful-dnd";
const TaskboardList = ({ title, cards, listID }) => {
return (
<Droppable droppableId={String(listID)}>
{provided => (
<div
className="taskboardlist_container"
{...provided.droppableProps}
ref={provided.innerRef}
style={styles.container}
>
<div className="sub-heading">{title}</div>
{cards.map((card, index) => (
<TaskboardCard
key={card.id}
index={index}
text={card.text}
id={card.id}
/>
))}
<TaskboardActionButton listID={listID} />
{provided.placeholder}
</div>
)}
</Droppable>
);
};
const styles = {
container: {
backgroundColor: "#eee",
width: 300,
padding: "0.5rem",
marginRight: "1rem",
height: "100%"
}
};
export default TaskboardList;
My TaskboardCard.js component
import React from "react";
import Card from "#material-ui/core/Card";
import Typography from "#material-ui/core/Typography";
import CardContent from "#material-ui/core/CardContent";
import { Draggable } from "react-beautiful-dnd";
const TaskboardCard = ({ text, id, index, sample }) => {
return (
<Draggable draggableId={String(id)} index={index}>
{provided => (
<div
className="taskboard_container"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Card>
<CardContent>
<Typography style={{ fontSize: "1.5rem" }} gutterBottom>
{text}
</Typography>
</CardContent>
</Card>
</div>
)}
</Draggable>
);
};
export default TaskboardCard;
For further reference to more files, I am attaching my GitHub link. Please consider visiting.
Any help would be much appreciated.
https://github.com/abhinav-anshul/consensolabs
And here is the link for a live demo https://consensolab.abhinavanshul.com/
// src/actions/index.js
export const CONSTANTS = {
ADD_CARD: "ADD_CARD",
ADD_LIST: "ADD_LIST",
DRAG_HAPPENED: "DRAG_HAPPENED",
DELETE_CARD: "DELETE_CARD"
};
// src/actions/cardActions.js
export const deleteCard = cardId => ({
type: CONSTANTS.DELETE_CARD,
payload: { cardId }
});
// src/reducers/listReducers.js
const listReducer = (state = initialState, action) => {
// ...
case CONSTANTS.DELETE_CARD: {
return {
...state,
cards: state.cards.filter(({id}) => id !== action.payload.cardId)
}
}
Then from your delete button, you just need to call dispatch(deleteCard(cardId))
Edit: I've updated your code sandbox to implement this https://codesandbox.io/s/stupefied-golick-7z78k

Categories

Resources