I have a react component which renders a list of covers and a button to delete the book from the list onClick. I'm passing in the delete action via map dispatch to props and the bookshelf info via map state to props. The delete action is successfully deleting the book from the database and is hitting my reducer. But even though the state updates, the book list component does not re-render (the deleted book doesn't disappear unless I reload).
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
import {CarouselProvider, Slider, Slide, ButtonBack, ButtonNext, Image, Dot} from 'pure-react-carousel';
class BookshelfIndexItem extends React.Component {
constructor(props){
super(props);
}
render() {
return(
<div>
<h3 className='shelf-title'>{this.props.bookshelf.name}</h3>
<CarouselProvider
naturalSlideWidth={135}
naturalSlideHeight={250}
totalSlides={this.props.bookshelf.book_ids.length}
visibleSlides={3}>
<Slider style={{height: '240px', display: 'flex', 'justifyContent': 'space-between'}}>
{this.props.bookshelf.book_ids.map((id, index) =>(<Slide index={index}
key={id} style={{'width': '150px','display': 'flex','alignItems': 'center', 'margin': '0 50px'}}>
<Link to={`/books/${id}`}>
<Image style={{height: '200px', width: '140px'}} src ={this.props.books[id].image_url}></Image>
</Link>
<button style={{height: '35px', 'marginLeft': '55px', 'marginTop': '5px'}}
onClick={() => this.props.deleteBookFromBookshelf({book_id: id, bookshelf_id: this.props.bookshelf.id})}>
X
</button>
</Slide>))}
</Slider>
<div className='slider-button-container'>
<ButtonBack className='slider-button'>back</ButtonBack>
<ButtonNext className='slider-button'>next</ButtonNext>
</div>
</CarouselProvider>
</div>
);
}
}
export default withRouter(BookshelfIndexItem);
I've tried a few of the lifecycle methods, such as fetching all the bookshelves in ComponentWillReceiveProps, but no change.
I have a strong suspicion I know what's up - any chance you're pushing to a copy of the array you're keeping in state and then setting state to that? When you push to an array (or add an item to a set or anything similar), you still end up with the same list object in memory (though with different contents). React is checking to see if there's a new list, and it sees the old list, sees the new list, and says "hey those are the same object in memory, so since they're the same I don't need to even trigger a shouldComponentUpdate.
I've produced a minimal example of this behavior so you can see it in action:
https://codesandbox.io/s/nrk07xx82j
If you hit the first button, you can see in the console that the new state is in fact correct, but the rendered list never updates (because of what I discussed above).
However, if you use array destructuring ([...list]), you end up returning the same array contents, but in a new array object, triggering a React update.
Hope that helps!
ninja edit: realizing I could be more specific since you're talking about deleting items - Array.pop and Array.splice() both behave in the same way, you're still dealing with the same object in memory and thus won't be triggering a component update.
Related
I'm currently learning React, and trying to get a sense of how components re-render. I have this parent component which renders three items. Each item just renders an <li>
function App() {
console.log("Parent Rerendered");
return (
<div>
<ul>
<Item1 />
<Item2 />
<Item3 />
</ul>
</div>
);
}
Item2 is a bit different because it also renders an "x" that will un-render the component when it's clicked:
function Item2() {
const [visible, setVisible] = useState(true);
const makeInvisible = () => {
setVisible(false);
};
console.log("Item 2 Rerendered");
return visible ? (
<div>
<li>
Second Item <span onClick={makeInvisible}>X</span>
</li>
</div>
) : null;
}
When I test this in my browser and click the "x", I can see from the console that Item2 gets re-rendered. However, none of the other components get re-rendered including the parent component. However the parent component does change, so how does this happen without re-rendering it.
If that's a bit confusing, here's an illustration of the initial state, my expectations, and reality. What am I misunderstanding about how React re-renders components?
A component rerenders when it sets state, or when its parent rerenders1. App has no state and no parent, so it will never rerender. It doesn't need to though. React saves the virtual DOM from the previous render, so it still knows that App is supposed to be a div surrounding a ul surrounding an Item1, Item2, and Item3. If the Item2 rerenders, and returns a null instead of a div, react will update the part of the real DOM that the Item2 is responsible for, by removing the div. The rest of the page remains intact
1) or if a context it consumes changes, or in a class component when you call forceUpdate. But for most cases, it's just state and parent that matter.
Instead of clicking onto <Item2 /> go to the Dev-tools -> Explorer -> select the Element and press delete. The view will also change, the gap will close, without react being involved at all.
React is responsible to update the DOM, the layout is done by the browser. So when <Item2 /> decides it wants to be rendered as null instead of a div>li (??? invalid markup ) and therefore removes the respective DOM-nodes, the browser will update the layout.
And the parent component has nothing to do with all that.
I am making a resume builder application and the whole structure was almost done.
Complete working codesandbox:
Here I have made components into stepper for each section,
index.js
<form onSubmit={handleSubmit}>
<Stepper
steps={sections}
activeStep={currentPage}
activeColor="red"
defaultBarColor="red"
completeColor="green"
completeBarColor="green"
/>
{currentPage === 1 && (
<>
<BasicDetails />
<button onClick={next}>Next</button>
</>
)}
{currentPage === 2 && (
<>
<EmploymentDetails />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<button onClick={prev}>Back</button>
<button onClick={next}>Next</button>
</div>
</>
)}
{currentPage === 3 && (
<>
<pre>{JSON.stringify(value, null, 2)}</pre>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<button onClick={prev}>Back</button>
<button onClick={handleSubmit}>Submit</button>
</div>
</>
)}
</form>
Steps to reproduce issue:
-> In Step 1 Enter First Name, Last Name, and Profile Summary
-> Click on Next button to move to next step.
-> Then click the back button to move backward to Step 1 (Currently in Step 2)
-> Here the values in First Name, Last Name are there but the value entered in text editor alone gets lost
Please refer the below image with text editor value entered for better understanding of the problem I am facing..
Text editor value alone gets lost if we switch forth/back the steps. But the entered value is stored in the form Context and not rendered in the Text Editor box.
Note:
Text editor is made as a component and it was used in Step 1 (For profile summary) and also in Step 2 (For employment description) and in both cases switching between steps, the value entered inside the text editor gets lost.
Analysis:
Based on my understanding, this happens because on navigating to other steps, the component get re-rendered and the EditorContainer component gets called and in text_editor.js it was given
this.state = {
editorState: EditorState.createEmpty(),
};
So it was created as empty.
So how can I control the component from getting re-rendered so that the data entered in text editor won't get lost.
Kindly please help me to retain the values entered inside the text editor. Big thanks in advance..
This happens because we're only saving our EditorContainer value to our Context, but we're not using it when we rerender the EditorContainer component.
The fix would be to pass the saved value as prop to our EditorContainer component.
Then before we render the EditorContainer, we'll convert that value to EditorState which can be done using convertFromHTML function, and set that as our editorState state.
Step 1: Pass value prop to EditorContainer
// basic_details.js
<EditorContainer
name="profileSummary"
value={basicDetails.profileSummary}
onChange={(event) => handleInputChange(event)}
/>
// employment_details.js
<EditorContainer
name="description"
value={inputField.description}
onChange={(event) => handleInputChange(index, event)}
/>
Step 2: Convert the value prop to EditorState
// text_editor.js
...
componentDidMount() {
// https://draftjs.org/docs/api-reference-data-conversion/#convertfromhtml
const { value } = this.props;
const blocksFromHTML = convertFromHTML(value);
const state = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap,
);
const editorState = EditorState.createWithContent(state);
this.setState({ editorState });
}
That's it! Check the demo below.
Edit Fix demo to check value if is string.
This is a great question. This is basically a design approach issue: For a scenario like yours, you need to design your component this way:
let's analyze the picture:
MAIN COMPONENT:
This is the component that should hold the state for the entire form-filling process. STE1-4 Are just views that allows you to input data that must all be updated in the main component. So This means, you must have state in main component and pass the state properties and props, including their update/setter methods.
STEP COMPONENT
This applies for all Step Components.
These components should do nothing except display the form step using state values received via props and update state by using setter methods, which are also received via props.
Conclusion:
Put your state in your main component, each step component should only display the form and update the main state. This means that by the time each step component is re-rendered, it will receive values updated in the main component. And you will achieve that via props.
It's very simple - you need to store editors state in your parent component. Try to use BasicDetails state for this.
I am building a ReactJS app using Google's Material-UI.
I have the following child class that is displayed in a Grid, after a search has been submitted. Depending if the search is one type or another, the ExpansionPanel inside this child class should be expanded or not expanded.
Here is the class that is being mapped in the parent component:
The expandByDefault boolean is passed from the parent class.
class SearchResults extends React.Component {
render () {
const { classes } = this.props;
const { batchAndContents } = this.props;
const { expandByDefault } = this.props;
return (
<div>
<ExpansionPanel defaultExpanded={expandByDefault} >
<ExpansionPanelSummary>
<Typography className={classes.heading}>Box {batchAndContents.sequenceCode}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<SearchResultsTable contents={batchAndContents.contents}/>
</ExpansionPanelDetails>
</ExpansionPanel>
</div>
)
}
}
Here is the render method for the parent class:
You can see in SearchResults, my custom class, I pass a prop named expandByDefault.
render () {
return (
<div>
. . . . .
. . . . .
. . . . .
<Grid container spacing={24} style={{padding: 24}}>
{this.state.searchResults.map((searchResult) => (
<Grid item key={searchResult.sequenceCode+searchResult.state} xs={12}>
<SearchResults batchAndContents={searchResult} expandByDefault={this.state.lastSearchType === "ContentBarcode"}/>
</Grid>
))}
</Grid>
</div>
)
}
I've tried several variations to get this to work, and I can't seem to understand what I'm missing.
What's interesting is that when I initially perform a search, and the ExpansionPanel's property defaultExpanded is set to true, it works. However if I don't refresh the page and perform another search with a different type, the results don't collapse back down when the type should cause that behavior.
Same behavior occurs if I initially perform the search and the ExpansionPanel defaultExpanded is set to false. It expands on click, and is default collapsed, however, when changing the search type to something that should cause default expanded panels, it doesn't work.
I appreciate any guidance.
I ran into the same problem. The property only seems to look at the value in the initial rendering.
If it's undefined or has a default value in the initial rendering then it will be used even when the value ofthis.props.expandByDefault is changed; its simply ignored.
You have to avoid rendering the component altogether until the prop has received the "correct" value if you want to use this feature.
It seems odd at first but it makes sense when you think about it. Once initially rendered the API doesn't want to risk overwriting actions taken by the user and accidentally close or open the dialog against one's will. It would probably lock the panel.
The defaultExpanded property only defines the default state of the component -- ie, whether or not to expand the panel when it is first rendered. Changes to this value later will not affect whether the panel is currently expanded or not.
To cause the panel to expand or collapse in response to changes in your app, you need to use the expanded property. From the docs:
If true, expands the panel, otherwise collapse it. Setting this prop enables control over the panel.
The problem here is that the keys that are on the <Grid item ... /> elements in the <Grid container /> parent view are not changing.
When a search is executed with a different type, the same data is being displayed, just displayed differently.
From my understanding, if react sees the same key with the same data, it doesn't need to re-render, even if there are states being passed as props to the children. In my case states are being passed as props to children in a <Grid item ... />.
As a resolution, I append the type of search to the key. So now when the search type changes, the key that holds the result does as well, and the children of the <Grid item ... /> are triggered to be re-rendered.
Sorry for not the best explanation, I have some practical experience with ReactJS, but I can not speak about it that well from an "under the hood" point of view.
Let's say I've got a Tooltip component that shows & hides depending on whether there is text data for it or not:
{this.state.tooltipText && (
<Tooltip
text={this.state.tooltipText} />
)}
If I wanted to transition this component in and out, I might instead include a boolean on prop and let the component manage transitioning out itself.
<Tooltip
on={this.state.tooltipText !== null}
text={this.state.tooltipText} />
With this setup, though, I lose the text prop right when the transition out should begin. There is very clearly never a state where on is false and I still have access to the text prop.
Is there a good way to handle this scenario?
Should the component keep an internal record of the previous state of tooltipText?
Or is it best to use two properties to track the state of the tooltip (tooltipOn and tooltipText) and never erase this.state.tooltipText?
Should the component keep an internal record of the previous state of tooltipText?
Yes. Otherwise the parent would have to decide when the transition starts / ends, which is not his authority.
Actually componentWillReceiveProps is best for this as you can access the current & next props:
componentWillReceiveProps(nextProps) {
if(this.props.text && !nextProps.text) {
this.fadeOut();
} else if(!this.props.text && nextProps.text) {
this.setState({ text: nextProps.text });
this.fadeIn();
}
}
Then the parent just has:
<Tooltip text={this.state.tooltipText} />
I have created a React application. In this application, I've created following components:
App Component (root) : where data is loaded into state
CardList Component: List of Cards, data is passed to it using props
Card Component: use forEach to pass data to Card and it has button
CustomButton Component: acts like a button with style
What I want is when the user clicks button on any of the Card, a number should get increased everytime. But I am not able to access Data here.
Can anyone help?
Here is the Card Component for your reference:
class Card extends Component {
render() {
return (
<div className="tc bg-light-green dib br3 pa3 ma2 grow bw2 shadow-5">
<h2>Votes: {this.props.votes}</h2>
<div>
<img src={this.props.pic} alt='Superstar'/>
<div>
<h2>{this.props.name}</h2>
<p>{this.props.email}</p>
</div>
</div>
<ActionButton btnText="Vote Now!" clickhandler={this.onVoteButtonClick}/>
</div>
);
}
onVoteButtonClick = (event) => {
console.log('It was clicked : '+this.props.id);
}
}
Two options which you should research more to see what suits your need best:
Redux (or something similar)
The new Context API available in React 16.
The gist of either solution is that you're managing application state independently of the dependent component tree(s). The Context API is arguably easier to implement whereas you'll currently find many more examples explaining the Redux approach as it's still the most common solution right now.