I'm having an issue with React passing state between sibling components when one of the siblings is deleted.
In my program, each component Plant has a state "watered", which has a default of "", and can be updated to the current day by pressing a button. When I delete a plant that has a non-empty watered state, that state passes to the next plant component.
I am sure that the correct item is being deleted by directly monitoring the keys in the parent.
Does this have something to do with a memory leak? Is there some code I can add to componentWillUnmount() method to solve this?
Thanks!
Edit:
My Plant class
class Plant extends React.Component {
state = {
watered : "",
note: "",
animate : "",
modalState: false
};
noteRef = React.createRef()
water = e => {
this.toggleAnimation()
const date = new Date();
const formatDate = date.getMonth().toString().concat('/', date.getDate())
this.setState({watered : formatDate})
}
toggleAnimation = () => {
this.setState({animate : "shake"});
setTimeout(() => {
this.setState({animate : ""})
}, 500);
}
componentDidMount() {
this.toggleAnimation()
}
componentWillUnmount() {
}
addNote = () => {
this.setState({modalState : true})
}
hideModal = () => {
const msg = "Variety : ".concat(this.noteRef.current.value)
this.setState({note:msg})
this.setState({modalState : false })
}
render() {
return (
<div>
<Modal show={this.state.modalState}>
<Modal.Header>Enter a variety for your plant!</Modal.Header>
<Modal.Body>
<input type="text" ref={this.noteRef} placeholder="Variety"/>
</Modal.Body>
<Modal.Footer>
<button onClick={this.hideModal}>Save</button>
</Modal.Footer>
</Modal>
<Card className={"plant-card".concat(this.state.animate)} >
<Card.Body className="card-body">
<Card.Title className="plant-card-title">
<span className="plant-card-title">{this.props.name}</span>
</Card.Title>
</Card.Body>
<Card.Text>
{this.state.note}
{this.state.watered}
<Container className="icon-div">
<img src={"images/watering-can.png"}
className="small-icon"
alt="can"
onClick={this.water}/>
<img src={"images/shovel.png"}
className="icon"
alt="shovel"
onClick={() => this.props.removeFromGarden(this.props.index)}/>
<img src={"images/grain-bag.png"}
className="icon"
alt="bag"
onClick={() => this.props.addHarvests(this.props.name, 1)}/>
<img src={"images/wheelbarrow.png"}
className="small-icon"
alt="bag"
onClick={this.addNote}/>
</Container>
</Card.Text>
</Card>
</div>
) }
export default Plant;`
Removing a plant from state in my App component (main parent)
removeFromGarden = key => {
const garden = {...this.state.garden };
delete garden[key]
this.setState({garden })
}
This might occur if you're using an index as a key of the components in the array. Make sure that after deleting an element, all remaining elements preserve their original keys - if not, react will interpret the next element as the deleted one.
There is going to be an unusual amount of code here because I am trying to share everything that is going on.
What I am trying to do is... In a list:
Mark services as complete.
Change their color and hide after completion.
Show hidden services on a button press.
I managed to hide each individual service, but couldn't work with the button that hides/shows all of the completed services.
I have a context provider:
const ContextBooking = React.createContext()
const ContextProviderBooking = ({ children }) => {
const [isHidden, setIsHidden] = useState(false); //sharing among both components to hide/show list
return <ContextBooking.Provider value={{ isHidden, setIsHidden }}>
{children}
</ContextBooking.Provider>
}
export { ContextBooking, ContextProviderBooking }
Which is being passed over the BookingsDisplay component in another file
...
<ContextProviderBooking>
<BookingsDisplay /> //this encapsulates each <Booking />
</ContextProviderBooking>
...
I am rendering each of the services in a larger component called 'BookingsDisplay'
const BookingsDisplay = () => {
const { isHidden, setIsHidden } = useContext(ContextBooking)
const display = day => //function that displays each service according to the day it was booked for
allBookings.map( //allBookings is a json file
item =>
item.day === day && (
<Booking
isHidden={isHidden}
completed={item.completed} //comes from json file, all default to false
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
)
return (
<div className="bookings">
<h2 className="ib">Next bookings</h2>
<button //This won't work as expected and hide/show all of the 'completed' bookings
onClick={() =>{
setIsHidden(!isHidden);}
}>
Show hidden
</button>
<h2>Today</h2>
<ul> {display('today')} </ul>
<h2> Tomorrow </h2>
<ul> {display('tomorrow')} </ul>
<h2> General </h2>
<ul> {display('other')} </ul>
</div>
)
}
Each 'Booking' component has a button that marks the service as complete. This happens by conditionally changing the class of each component. This works fine as far as I'm concerned
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.isHidden)
console.log(props.isHidden) // will output true or false 16 times(there are 16 component in total)
const [isCompleted, setIsCompleted] = useState(props.completed);
return (
<li
className={
isCompleted && isHidden ? 'booking-complete hide' //class names are not changing individually
: isCompleted ? 'booking-complete' //if button is pressed on one of them,
: 'booking' //it may affect the other
}
key={props.id}
id={props.id}>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
<button
onClick={() => { //shouldn't this button work of each li and not sometimes all of them?
if (!isCompleted && !isHidden) {
setIsCompleted(!isCompleted); //this changes color of the service as className changes
setTimeout(() => setIsHidden(!isHidden), 200) //after a short time it is hidden
}
else if (isCompleted && !isHidden) {
setIsCompleted(!isCompleted);
}
else {
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);
}
}}>
{!isCompleted ? `Completed` : `Not complete`}
</button>
</li>
)
}
There're two kind of isHidden in your app. I'mma call the one in context the global hidden isAllHidden and the one in <Booking /> the local hidden isHidden.
The problem is you misuse the two. local hidden is an internal state of <Booking />. The reason of it's existence is because you need that 200ms delay of animation, otherwise it can be replaced by isCompleted. So it should be derived from isCompleted instead of isAllHidden.
Fix 1:
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.completed)
}
Now global hidden and local hidden combine to decide whether a Booking should hide. You logic should reflect this fact.
Fix 2:
const shouldBeHidden = Boolean(props.isAllHidden && isHidden)
return (
<li
className={
isCompleted && shouldBeHidden ? 'booking-complete hide'
: isCompleted ? 'booking-complete'on one of them,
: 'booking'other
}
>
...
Put together:
const Booking = (props) => {
const [isHidden, setIsHidden] = useState(props.completed)
const [isCompleted, setIsCompleted] = useState(props.completed)
const shouldBeHidden = props.isAllHidden && isHidden
return (
<li
className={
isCompleted && shouldBeHidden ? 'booking-complete hide' //class names are not changing individually
: isCompleted ? 'booking-complete' //if button is pressed on one of them,
: 'booking' //it may affect the other
}
>
<input type='checkbox' checked={isCompleted} onChange={() => {
setIsCompleted(!isCompleted)
setTimeout(() => setIsHidden(!isHidden), 200)
}}/>
<span>{props.name}</span>
</li>
)
}
I setup a demoboard here to show the result.
I have the following piece of code for my component. The desired behaviour for the button is to change the className for each li, but this is not working.
const Booking = (props) => {
let { hidden } = useContext(ContextBooking)
let completed = props.completed
return (
<li
className={ //should change according to the button click below
completed && hidden ?
'booking-complete hide'
: completed ?
'booking-complete'
:
'bookings'
}}
key={props.id}
id={props.id}
>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
</li>
)
}
{!completed && (
<button
onClick={() => {
if (!completed && !hidden) {
completed = !completed //does make it false
hidden = !hidden //does make it false
} //above works, but won't change classname for each 'li'
else if (completed && hidden) {
completed = !completed
hidden = !hidden
}
}}>
Complete
</button>
)}
In another component, I am creating multiple of these 'Booking' components, by filling in the details with info that come from a json file
const DisplayBookings = () => {
const display = (day) => allBookings.map(item => //allBookings is a json file
item.day === day &&
<Booking
completed={item.completed}
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
I emphasised json file as I believe it could be the source of the problem?
A component can in most cases not update its own props, and doing so even if possible is an antipattern.
You can instead use state for updating the components state.
You can create hooks for setting state like this:
const [isCompleted, setIsCompleted] = useState(props.completed);
const [isHidden, setIsHidden] = useState(hidden);
Then in your onClick you use this to update the values:
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);
I am making a menu and submenus using recursion function and I am in the need of help to open only the respective menu and sub menu's..
For button and collapse Reactstrap has been used..
Recursive function that did menu population:
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<div key={item.id}>
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{this.buildMenu(item.children)}
</Collapse>
</div>
);
})}
And the buildMenu function as follows,
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<div>
{this.state.isToggleOpen}
<Button onClick={this.toggle.bind(this)}> {item.name} </Button>
<Collapse isOpen={this.state.isToggleOpen}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</Collapse>
</div>
</li>
))}
</ul>
);
}
There is no problem with the code as of now but I am in the need of help to make menu -> submenu -> submenu step by step open and closing respective levels.
Working example: https://codesandbox.io/s/reactstrap-accordion-9epsp
You can take a look at this example that when you click on any menu the whole level of menus gets opened instead of clicked one..
Requirement
If user clicked on menu One, then the submenu (children)
-> One-One
needs to get opened.
And then if user clicked on One-One,
-> One-One-One
-> One - one - two
-> One - one - three
needs to get opened.
Likewise it is nested so after click on any menu/ children their respective next level needs to get opened.
I am new in react and reactstrap way of design , So any help from expertise would be useful for me to proceed and learn how actually it needs to be done.
Instead of using one large component, consider splitting up your component into smaller once. This way you can add state to each menu item to toggle the underlying menu items.
If you want to reset al underlying menu items to their default closed position you should create a new component instance each time you open up a the underlying buttons. By having <MenuItemContainer key={timesOpened} the MenuItemContainer will be assigned a new key when you "open" the MenuItem. Assigning a new key will create a new component instance rather than updating the existing one.
For a detailed explanation I suggest reading You Probably Don't Need Derived State - Recommendation: Fully uncontrolled component with a key.
const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);
const {Component, Fragment} = React;
const {Button, Collapse} = Reactstrap;
class Menu extends Component {
constructor(props) {
super(props);
this.state = {menuItems: []};
}
render() {
const {menuItems} = this.state;
return <MenuItemContainer menuItems={menuItems} />;
}
componentDidMount() {
loadMenu().then(menuItems => this.setState({menuItems}));
}
}
class MenuItemContainer extends Component {
render() {
const {menuItems} = this.props;
if (!menuItems.length) return null;
return <ul>{menuItems.map(this.renderMenuItem)}</ul>;
}
renderMenuItem(menuItem) {
const {id} = menuItem;
return <li key={id}><MenuItem {...menuItem} /></li>;
}
}
MenuItemContainer.defaultProps = {menuItems: []};
class MenuItem extends Component {
constructor(props) {
super(props);
this.state = {isOpen: false, timesOpened: 0};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
render() {
const {name, children} = this.props;
const {isOpen, timesOpened} = this.state;
return (
<Fragment>
<Button onClick={isOpen ? this.close : this.open}>{name}</Button>
<Collapse isOpen={isOpen}>
<MenuItemContainer key={timesOpened} menuItems={children} />
</Collapse>
</Fragment>
);
}
open() {
this.setState(({timesOpened}) => ({
isOpen: true,
timesOpened: timesOpened + 1,
}));
}
close() {
this.setState({isOpen: false});
}
}
ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>
<div id="root"></div>
You will want to create an inner component to manage the state at each level.
For example, consider the following functional component (I'll leave it to you to convert to class component):
const MenuButton = ({ name, children }) => {
const [open, setOpen] = useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</>
);
};
This component will manage whether to display its children or not. Use it in place of all of your <div><Button/><Collapse/></div> sections, and it will manage the open state for each level.
Keep shared state up at the top, but if you don't need to know whether something is expanded for other logic, keep it localized.
Also, if you do need that info in your parent component, use the predefined object you already have and add an 'open' field to it which defaults to false. Upon clicking, setState on that object to correctly mark the appropriate object to have the parameter of true on open.
Localized state is much cleaner though.
Expanded Example
import React, { Component, useState, useCallback, Fragment } from "react";
import { Collapse, Button } from "reactstrap";
import { loadMenu } from "./service";
const MenuButton = ({ name, children }) => {
const [open, setOpen] = React.useState(false);
const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
return (
<Fragment>
<Button onClick={toggle}>{name}</Button>
<Collapse open={open}>{children}</Collapse>
</Fragment>
);
};
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
currentSelection: "",
menuItems: [],
};
}
componentDidMount() {
loadMenu().then(items => this.setState({ menuItems: items }));
}
buildMenu(items) {
return (
<ul>
{items &&
items.map(item => (
<li key={item.id}>
<MenuButton name={item.name}>
{item.children && item.children.length > 0
? this.buildMenu(item.children)
: null}
</MenuButton>
</li>
))}
</ul>
);
}
render() {
return (
<div>
<h2>Click any of the below option</h2>
{this.state.menuItems &&
this.state.menuItems.map((item, index) => {
return (
<MenuButton name={item.name}>
{this.buildMenu(item.children)}
</MenuButton>
);
})}
</div>
);
}
}
export default Hello;
I am using a function to filter values.
If I map over the function filteredMovies, it removes the genre buttons and movies which do not apply.
However, instead of removing them I want to a custom className. I want to apply a different style to <Filter1> & <Movies>. What is the correct way to do this?
I've put this inside a codesandbox (https://codesandbox.io/s/76ZjOyBvA)
Example 1:
Compare this.state.movies
{this.uniqueGenres1(this.state.movies).map(e => (
<Filter1
key={e.id}
/>
))}
To filteredMovies
{this.uniqueGenres1(filteredMovies).map(e => (
<Filter1
key={e.id}
/>
))}
For the values that do NOT appear in filteredMovies, apply a different className
Use the className attribute of React:
class Filter1 extends React.Component {
handleClick = () => {
this.props.onChange(this.props.genre.name);
};
render() {
const { genre, isActive } = this.props;
return (
<button onClick={this.handleClick} className={isActive? 'active' : 'inactive'}>
{genre.name}
{' '}
<strong>{isActive ? 'Active' : 'Inactive'}</strong>
</button>
);
}
}
Also, don't forget to change 'filteredMovies' to 'movies' in the second map:
<h3> Using filteredMovies function</h3>
{this.uniqueGenres1(movies).map(e => (
<Filter1
key={e.id}
genre={e}
isActive={!!selectedFilters[e.name]}
value={e.type}
onChange={this.handleChange}
/>
))}
Update:
To apply a css style to the <li>s rather than hiding them, change the relevant sections in your code to these:
class Result extends React.Component {
render() {
const { result, isActive } = this.props;
return (
<div>
<li className={isActive? 'active' : 'inactive'}>
{result.name} {' '}
({result.genres.map(x => x.name).join(', ')}){' '}
</li>
</div>
);
}
}
{movies.map(movie => {
var isActive = !!filteredMovies.find(filteredrMovie => filteredrMovie.name === movie.name);
return (
<Result key={movie.id} result={movie} isActive={isActive} />
)
})}
It basically uses the JS find function to know if a movie from the filtered lists exists in the entire one. Then it passes the result as a prop to the Result component.
Update2:
In order to style your buttons based on rather or not they exist in the results rather than if they active:
<h3>Using filteredResults function</h3>
{this.uniqueGenres(filteredResults).map(genre => {
const isExisting = !!this.uniqueGenres(allFilters).find(
genre1 => genre1 === genre,
);
return (
<Filter1
key={genre.id}
genre={genre}
isActive={!!selectedFilters[genre.name]}
isExisting={isExisting}
onChange={genre.handleChange}
/>
)})}
Replace 'allFilters' with the correct name of the variable that contains the complete list.
Update3:
It's working for me now after changing the code of Filter1 component, so it will resolve the className based on 'isExisting':
class Filter1 extends React.Component {
handleClick = () => {
this.props.onChange(this.props.genre);
};
render() {
const { genre, isActive, isExisting } = this.props;
return (
<button onClick={this.handleClick} className={isExisting ? 'existing' : 'notExisting'}>
{genre.name}
{' '}
<strong>{isActive ? 'Active' : 'Inactive'}</strong>
</button>
);
}
}