Component is not re-rendering after state changed - javascript

I'm trying to fetch videos from youtube and display on browser.. I'm able to get videos and update state property. ( checked with console log)
But SearchResults component is not re-rendering when I update the state property. Here is my component
class Application extends React.Component {
state = {videos: []};
handleFormSubmit = async term => {
const res = await youtubeApi.get("search", {
params: {q: term}
});
this.setState({videos: res.data.items})
};
render() {
return (
<div className="container">
<SearchForm onFormSubmit={this.handleFormSubmit}/>
<SearchResults videos={this.state.videos}/>
</div>
)
}
}
SearchResults component
class SearchResults extends React.Component {
state = {featured: null, suggested: []};
componentDidMount() {
if (this.props.videos.length > 0)
this.setState({featured: this.props.videos[0].id.videoId});
if (this.props.videos.length > 1)
this.setState({suggested: this.props.videos.slice(1)});
}
handleSidebarVideoClick = id => {
this.setState({featured: id})
};
render() {
return (
<div className="row">
<div className="col-md-8">
<Video video={this.state.featured}/>
</div>
<div className="col-md-4">
<Sidebar videos={this.state.suggested} onSidebarVideoClick={this.handleSidebarVideoClick}/>
</div>
</div>
)
}
}

You need to have getDerivedStateFromProps() in your SearchResults component in order to expect a new props value when it changes on parent component.
ComponentDidMount is called once the component is mounted, not when it receives prop values when it changes every time.
So you should have something like this:
static getDerivedStateFromProps(nextProps) {
if (nextProps.videos.length > 0) {
return {
featured: nextProps.videos[0].id.videoId
};
}
if (nextProps.videos.length > 1) {
return {
suggested: nextProps.videos.slice(1)
};
}
return null;
}
Ref: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

It is unlikely you checked the component's state property. You most likely checked this.state which does get updated. The question is what is this.
There is no evidence you bound your event handlers to the component as recommended in the dev guide. So the keyword this doesn't reflect the Component, it reflects the DOM element receiving the event.
To test this, console log the value of this in you event handlers.
To fix:
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleSidebarVideoClick = this.handleSidebarVideoClick.bind(this);
in their respective classes constructors.
Ref: https://reactjs.org/docs/handling-events.html

Related

shouldComponentUpdate() is not being called

Problem
I've parent class which contains list of items and renders component for each item of the list. When some item has been changed (even only one), all items in the list are being rerendered.
So I've tried to implement shouldComponentUpdate(). I am using console.log() to see if it is called but I can't see the log. I've found question shouldComponentUpdate is not never called and tried to return return (JSON.stringify(this.props) !=JSON.stringify(nextProps)); but component still renders itself again. So I've tried just to return false (like do not ever update) but it still does. As the last try I've used PureComponent but it is still being rerendered.
Question
How can I stop children re-rendering if the parent list changes and why is ShouldComponentUpdate never called?
Edit
I've noticed something what I didn't mention in question, I'm sorry for that. I am using context. If I don't use context -> it's ok. Is there any chance to stop re-render while using context? (I'm not using context on updated item - values of context didn't change).
Example
I've parent class which iterates list and renders TaskPreview component for each item of list:
class Dashboard extends React.Component
{
constructor(props) {
this.state = {
tasks: {},
};
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: {...prevState.tasks, [task._id]: task}
}));
// ... some code
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: this.props.tasks.map((task,index) =>
<TaskPreview key={task._id} task={task} onChange={this.onTaskUpdate}/>
})}
</div>
)
}
}
and I've children TaskPreview class:
class TaskPreview extends React.Component
{
shouldComponentUpdate(nextProps) {
console.log('This log is never shown in console');
return false; // just never!
}
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
TaskPreview.contextType = TasksContext;
export default TaskPreview;
As #Nicolae Maties suggested I've tried to use Object.keys for iteration instead of direct map but it still doesn't call "shouldComponentUpdate" and still being re-rendered even if there is no changes.
Updated code:
render() {
return (
<div>
{(!Object.entries(this.props.tasks).length)
? null
: Object.keys(this.props.tasks).map((key,index) => {
let task = this.props.tasks[key];
<TaskPreview key={task._id} task={task}/>
}
})}
</div>
)
}
Component is being re-rendered because of .contextType.
TaskPreview.contextType = TasksContext;
Also as is mentioned in documentation:
The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update. Source: reactjs.org/docs/context
You have to use context somehow else or do not use it at all.
You can use Context.Consumer which won't force re-render of current component but it might force re-render of its children.
<TasksContext.Consumer>
{value => /* render something based on the context value */}
</TasksContext.Consumer>
Instead of return (JSON.stringify(this.props) != JSON.stringify(nextProps)); in your shouldComponentUpdate() life cycle, try specifying tasks object like this return (JSON.stringify(this.props.tasks) != JSON.stringify(nextProps.tasks));
Maybe react is creating new instances of your component and replaces the old instances with them. That's why you're probably not getting your lifecycle method invoked. That can happen if the key property you're assigning in the map always changes.
use from pureComponent and array as state:
class Dashboard extends React.PureComponent
{
constructor(props) {
this.state = {
tasks: this.props.tasks
}
}
onTaskUpdate=(task)=>
this.setState(prevState => ({
tasks: [...prevState.tasks, task] // render only new task
}));
render() {
const {tasks} = this.state
return (
<div>
{tasks.map(task => <TaskPreview key={task._id} task={task} />)}
</div>
)
}
}
class TaskPreview extends React.PureComponent
{
render() {
console.log('task rendered:',this.props.task._id); // indicates rerender
return(<div>Something from props</div>);
}
}
In the shouldComponentUpdate() method of your TaskPreview component, you should check if the next props have changes in comparison to the current props. Then if there are changes, return true to update the component, otherwise false.
The following example compares all the fields of props object with the new props object. But you can only check the props you are interested in.
shouldComponentUpdate(nextProps) {
return !!(Object.keys(nextProps).find(key => nextProps[key] !== this.props[key]));
}
I tried with below code snippet, shouldComponentUpdate worked as I expected. Could you share your Dashboard initial props ?
class Dashboard extends React.Component {
constructor(props) {
this.state = {
tasks: {}
};
}
onTaskUpdate = task =>
this.setState(prevState => ({
tasks: { ...prevState.tasks, [task._id]: task }
}));
// ... some code
render() {
return (
<div>
{!Object.entries(this.props.tasks).length
? null
: Object.keys(this.props.tasks).map((key, index) => {
let task = this.props.tasks[key];
return (
<TaskPreview
key={task._id}
task={task}
onChange={this.onTaskUpdate}
/>
);
})}
</div>
);
}
}
class TaskPreview extends React.Component {
shouldComponentUpdate(nextProps) {
console.log("This log is never shown in console");
return nextProps.task._id != this.props.task._id;
}
render() {
console.log("task rendered:", this.props.task); // indicates rerender
return (
<button onClick={() => this.props.onChange(this.props.task)}>
Something from props
</button>
);
}
}
my initial props for Dashboard component is :
<Dashboard tasks={{test:{_id:'myId', description:'some description'}}}/>

How high should state be lifted in React?

Should intermediate components control parts of state and call props passed to them or should state be lifted higher? I've been going back and forth whether to have the child component utilize local state or have it handled by higher component and pass additional props down.
In this limited example, I have a Main component. I display some data in this component and pass functions to filter the data to a child component. Though, main component doesn't necessarily need to know about when the menuOpen property is changed. However, I need to update menuOpen when handleCancel(), handleSave(), and handleButtonClick() are called.
handleCancel() and handleSave() both modify the data that is displayed so I declare them in the Main component.
Should I be passing all these props through from Main component or use intermediate components to handle smaller portions of local state but also call props from a parent (grandparent etc) component?
Main Component
//Parent component
class Main extends React.Component {
constructor() {
super();
this.state = {
checkBoxes: {
1: {
name: 'Apple',
isChecked: true,
},
//...
},
fruit: {
1: {
name: 'Apple',
},
//...
},
checkedBoxes: [],
};
this.baseState = JSON.stringify(this.state.checkBoxes);
this.fruitFilter = this.fruitFilter.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleChange = this.handleChange.bind(this);
this.resetState = this.resetState.bind(this);
}
resetState() {
this.setState({checkBoxes: JSON.parse(this.baseState)});
}
//populates the checkedboxs array with name to filter by
handleSave() {
const checkedBoxes = Object.keys(this.state.checkBoxes)
.filter(key => {
//....some logic
});
this.baseState = JSON.stringify(this.state.checkBoxes);
this.setState({checkedBoxes: checkedBoxes});
}
//handles the checkbox toggle
handleChange(e) {
const checkBoxes = {...this.state.checkBoxes};
checkBoxes[e.target.id].isChecked = e.target.checked;
this.setState({checkBoxes: checkBoxes});
}
//filteres the fruit - if nothing is checked return them all
fruitFilter(fruit) {
return Object.keys(fruit)
.filter(key => {
//...filter logic
})
}
render() {
const visibleFruits = this.fruitFilter(this.state.fruit);
return (
<div>
<Filter
resetState={this.resetState}
checkBoxes={this.state.checkBoxes}
handleSave={this.handleSave}
handleChange={this.handleChange}
/>
<div>
<h2>Filtered Fruit</h2>
{Object.keys(visibleFruits).map(key => {
return (
//... renders list of fruit
);
})}
</div>
</div>
);
}
}
Child Component
class Filter extends React.Component {
constructor(props) {
super(props);
this.state = {
menoOpen: false,
};
this.handleCancel = this.handleCancel.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleSave() {
this.setState({menuOpen: false});
this.props.handleSave();
}
handleCancel() {
this.setState({menuOpen: false});
this.props.resetState();
}
handleButtonClick() {
this.setState({menuOpen: !this.state.menuOpen});
}
render() {
return (
<div>
<button onClick={this.handleButtonClick}>Choose Fruits</button>
{this.state.menuOpen && (
<FilterMenu
checkBoxes={this.props.checkBoxes}
handleSave={this.handleSave}
handleCancel={this.handleCancel}
handleChange={this.props.handleChange}
/>
)}
</div>
);
}
}
Grandchild Component
const FilterMenu = ({checkBoxes, handleChange, handleCancel, handleSave}) => {
return (
<div>
{Object.keys(checkBoxes).map(key => {
return (
//... renders dropdown menu
);
})}
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
);
};
Refine the separation of concerns and I think you'll like it better.
Define all checkbox event handlers in Filter.
Filter communications with Main via state only.
Don't force Main to evaluate UI components to set state.
Define Main state for Filter to use as needed to avoid the above.
Filter will construct the checkboxes.
Cancel and Save buttons seem like Filter level functions to me.
A FilterMenu component now seems pointless because it does not do anything. Perhaps in the larger architecture it is useful but you can always re-factor it out of Filter when needed
Filter component is the seam in the code that separates action from state.
State is not unnecessarily pushed further down.
Actual functionality is not unnecessarily pushed further up.
Coupling between Main and Filter is reduced. Filter has more reuse potential.

Rendering react component after api response

I have a react component that I wish to populate with images using the Dropbox api. The api part works fine, but the component is rendered before the data comes through & so the array is empty. How can I delay the rendering of the component until it has the data it needs?
var fileList = [];
var images = [];
var imageSource = [];
class Foo extends React.Component {
render(){
dbx.filesListFolder({path: ''})
.then(function(response) {
fileList=response.entries;
for(var i=0; i<fileList.length; i++){
imageSource.push(fileList[0].path_lower);
}
console.log(imageSource);
})
for(var a=0; a<imageSource.length; a++){
images.push(<img key={a} className='images'/>);
}
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
Thanks for your help!
Changes:
1. Don't do the api call inside render method, use componentDidMount lifecycle method for that.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
2. Define the imageSource variable in state array with initial value [], once you get the response update that using setState, it will automatically re-render the component with updated data.
3. Use the state array to generate the ui components in render method.
4. To hold the rendering until you didn't get the data, put the condition inside render method check the length of imageSource array if length is zero then return null.
Write it like this:
class Foo extends React.Component {
constructor(){
super();
this.state = {
imageSource: []
}
}
componentDidMount(){
dbx.filesListFolder({path: ''})
.then((response) => {
let fileList = response.entries;
this.setState({
imageSource: fileList
});
})
}
render(){
if(!this.state.imageSource.length)
return null;
let images = this.state.imageSource.map((el, i) => (
<img key={i} className='images' src={el.path_lower} />
))
return (
<div className="folioWrapper">
{images}
</div>
);
}
}
You should be using your component's state or props so that it will re-render when data is updated. The call to Dropbox should be done outside of the render method or else you'll be hitting the API every time the component re-renders. Here's an example of what you could do.
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
imageSource: []
}
}
componentDidMount() {
dbx.filesListFolder({ path: '' }).then(function(response) {
const fileList = response.entries;
this.setState({
imageSource: fileList.map(file => file.path_lower);
})
});
}
render() {
return (
<div className="folioWrapper">
{this.state.imageSource.map((image, i) => <img key={i} className="images" src={image} />)}
</div>
);
}
}
If there are no images yet, it'll just render an empty div this way.
First off, you should be using the component's state and not using globally defined variables.
So to avoid showing the component with an empty array of images, you'll need to apply a conditional "loading" class on the component and remove it when the array is no longer empty.

Maintaining Component Refs Through React.cloneElement

I have been testing the possible limitations/dangers of using React.cloneElement() to extend a component's children. One possible danger I've identified is the possible overwriting of props such as ref and key.
However, as per React's 0.13 release candidate (back in 2015):
However, unlike JSX and cloneWithProps, it also preserves refs. This means that if you get a child with a ref on it, you won't accidentally steal it from your ancestor. You will get the same ref attached to your new element.
[...]
Note: React.cloneElement(child, { ref: 'newRef' }) DOES override the ref so it is still not possible for two parents to have a ref to the same child, unless you use callback-refs.
I have written a small React application that clones children components pushed through, testing for the validity of refs at two levels:
class ChildComponent extends React.Component{
constructor(props){
super(props);
this.onClick = this.onClick.bind(this);
this.extendsChildren = this.extendChildren(this);
}
onClick(e) {
e.preventDefault();
try{
alert(this._input.value);
}catch(e){
alert('ref broken :(');
}
}
extendChildren(){
return React.Children.map(this.props.children, child => {
return React.cloneElement(
child,
{
ref: ref => this._input = ref
}
);
});
}
render() {
return(
<div>
<button onClick={this.onClick}>
ChildComponent ref check
</button>
{this.extendChildren()}
</div>
);
}
}
class AncestorComponent extends React.Component{
constructor(props){
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(e) {
e.preventDefault();
try{
alert(this._input.value);
}catch(e){
alert('ref broken :(');
}
}
render() {
return (
<div>
<p>
The expected behaviour is that I should be able to click on both Application and ChildComponent check buttons and have a reference to the input (poping an alert with the input's value).
</p>
<button onClick={this.onClick}>
Ancestor ref check
</button>
<ChildComponent>
<input ref={ref => this._input = ref} defaultValue="Hello World"/>
</ChildComponent>
</div>
);
}
}
However, cloningElements inside my ChildComponent overwrites the AncestorComponent's ref prop from the input field, where I would expect that ref prop to be preserved, alongside the new ref I defined as part of the React.cloneElement.
You can test this by running the CodePen.
Is there anything I'm doing wrong, or has this feature been dropped since?
As per Dan Abramov's response, overwriting the reference, even with a callback, is still going to overwrite the reference. You'll need to call the current reference as part of the callback declaration:
return React.Children.map(this.props.children, child =>
React.cloneElement(child, {
ref(node) {
// Keep your own reference
this._input = node;
// Call the original ref, if any
const {ref} = child;
if (typeof ref === 'function') {
ref(node);
}
}
)
);

React render method that depends on asynchronous request and state change

I am trying to learn ReactJS and Redux, and have come across a problem that I cannot seem to get over.
I have a React component, that gets data from an asynchronous request.
export class MyPage extends React.Component {
constructor(props) {
super(props)
this.state = {
enableFeature: false,
}
this.handleEnableFeatureChange = this.handleEnableFeatureChange.bind(this)
}
componentWillMount () {
this.fetchData()
}
fetchData () {
let token = this.props.token
this.props.actions.fetchData(token)
}
handleEnableFeatureChange (event) {
this.setState({ enableFeature: event.target.checked })
}
render () {
if (this.props.isFetching) {
return (
<div>Loading...</div>
)
} else {
return (
<div>
<label>Enable Feature
<input type="checkbox"
className="form-control"
checked={this.props.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
</div>
)
}
}
}
So, my problem now is that, when I change the state of the checkbox, I want to update the state of my data. However, every time I update the state of my data, the react component method shouldComponentUpdate kicks in, and uses the current props to render the original data.
I would like to see how such cases are handled in general.
Thanks.
Try to do it like the following, i.e.
Use componentWillReceiveProps to assign props.enableFeature to state.enableFeature. From documentation
componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render.
componentWillReceiveProps() is not invoked if you just call this.setState()
Use this state to load the value of checkbox
Manipulate this state (onchange) to update the value of checkbox
Following code can work in your case
export class MyPage extends React.Component {
static propTypes = {
isFetching: React.PropTypes.bool,
enableFeature: React.PropTypes.bool,
token: React.PropTypes.string,
actions: React.PropTypes.shape({
fetchData: React.PropTypes.func
})
};
state = {
enableFeature: false,
};
componentWillMount () {
this.fetchData();
}
/* Assign received prop to state, so that this state can be used in render */
componentWillReceiveProps(nextProps) {
if (this.props.isFetching && !nextProps.isFetching) {
this.state.enableFeature = nextProps.enableFeature;
}
}
fetchData () {
const { token } = this.props;
this.props.actions.fetchData(token)
}
handleEnableFeatureChange = (event) => {
this.setState({ enableFeature: event.target.checked })
};
render () {
return (<div>
{ this.props.isFetching && "Loading..." }
{
!this.props.isFetching && <label>
Enable Feature
<input
type="checkbox"
className="form-control"
checked={this.state.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
}
</div>);
}
}
Note: The above code was not executed, but should work (babel's stage-0 code)
Change it to checked={this.state.enableFeature}

Categories

Resources