Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

My Redux state has changed, why doesn't React trigger a re-render?

I am trying to design a notification component where notifications will appear on certain occasions (like connections problems, successful modifications, etc.).

I need the notifications to disappear after a couple of seconds, so I am triggering a state change to delete the notification from Redux state from setTimeout inside the notification's componentDidMount.

I can see that the state does change, but React-Redux is not re-rendering the parent component so the notification still appears on the DOM.

Here is my Redux reducer:

const initialState = {     notifications: [] }  export default function (state = initialState, action) {   switch(action.type) {     case CLEAR_SINGLE_NOTIFICATION:       return Object.assign ({}, state, {         notifications: deleteSingleNotification(state.notifications, action.payload)       })       case CLEAR_ALL_NOTIFICATIONS:         return Object.assign ({}, state, {           notifications: []         })       default:         return state     } }  function deleteSingleNotification (notifications, notificationId) {   notifications.some (function (notification, index) {     return (notifications [index] ['id'] === notificationId) ?            !!(notifications.splice(index, 1)) :            false;   })    return notifications; } 

and my React components (Main and Notification):

/* MAIN.JS */ class Main extends Component {      renderDeletedVideoNotifications() {         console.log('rendering notifications');         const clearNotification = this.props.clearNotification;         return this.props.notifications.map((notification)=> {             return <Notification                 key={notification.id}                 message={notification.message}                 style={notification.style}                 clearNotification={clearNotification}                 notificationId={notification.id}             />         });     }      render() {         console.log('rerendering');         return (             <div className="_main">                 <Navbar location={this.props.location} logStatus={this.props.logStatus}                         logOut={this.logout.bind(this)}/>                 <div className="_separator"></div>                 {this.props.children}                 <BottomStack>                     {this.renderDeletedVideoNotifications()}                 </BottomStack>             </div>         );     }  }  function mapStateToProps(state) {     return {logStatus: state.logStatus, notifications: state.notifications.notifications}; }  function mapDispatchToProps(dispatch) {     return bindActionCreators({checkLogStatus, logOut, clearNotification, clearAllNotifications}, dispatch); }  export default connect(mapStateToProps, mapDispatchToProps)(Main);  /* NOTIFICATION.JS */  export default class Notification extends Component{     constructor(props){         super(props);         this.state = {show: true}     }      componentWillReceiveProps(nextProps){         if(nextProps.message){             this.setState({show: true});         }     }      clearNotification(notificationId){         this.props.clearNotifications(notificationId);     }      componentDidMount(){         console.log('notification  mount');         setTimeout(()=>{             console.log('timed out');             this.props.clearNotification(this.props.notificationId);         }, 1000);     }      closeNotification(){         this.props.clearNotification(this.props.notificationId);         this.setState({show: false});     }      render(){         const notificationStyles = () =>{             if (this.props.style === "error"){                 return {backgroundColor: 'rgba(152, 5, 19, 0.8)'}             }             return {backgroundColor: 'rgba(8, 130, 101, 0.8)'}         };          if(!this.state.show){             return null;         }         return (             <div className="notification" style={notificationStyles()}>                 <div className="notificationCloseButton" onClick={this.closeNotification.bind(this)}>                     <i className="material-icons">close</i>                 </div>                 {this.props.message}             </div>         )     }  }; 
like image 945
Nicolas Avatar asked Sep 15 '16 14:09

Nicolas


People also ask

Does Redux state change cause re-render?

Any time any piece of the Redux state is updated, our Component will re-render, regardless of whether the store update was related to our component or not.

Does changing state cause re-render?

There are four reasons why a component would re-render itself: state changes, parent (or children) re-renders, context changes, and hooks changes. There is also a big myth: that re-renders happen when the component's props change.

Does React re-render if state changes?

React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically.

Why is React not updating on state change?

State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass.


1 Answers

You've got everything hooked up correctly, but you're missing one key concept for Redux:

With Redux, you never mutate any part of state.

From the Redux guide:

Things you should never do inside a reducer:

  • Mutate its arguments;
  • Perform side effects like API calls and routing transitions;
  • Call non-pure functions, e.g. Date.now() or Math.random().

In deleteSingleNotification, you're using .splice to cut the old notification out of your array. Instead, you need to return a brand new array with the unwanted notification missing from it. The easiest way to do this is with the .filter function:

function deleteSingleNotification(notifications, notificationId){     return notifications.filter (notification => {         return notification.id !== notificationId     } } 
Here is a JSBin with your working notification system!

So here is why this works: React-Redux's job is to update your components whenever a specific part of your Redux store is changed. It uses a === test on every part of the state tree to know if anything changed.

When you go and change the state with something like .splice, it checks and thinks nothing is different.

Here's an example to demonstrate the problem:

var array = [ 'a', 'b', 'c' ]  var oldArray = array  array.splice (1, 1) // cut out 'b'  oldArray === array // => true!  Both arrays were changed by using .splice,                    // so React-Redux *doesn't* update anything 

Instead, React-Redux needs us to do this:

var array = [ 'a', 'b', 'c' ]  var oldArray = array  array = array.filter (item, index => index !== 1) // new array without 'b'  oldArray === array // false.  That part of your state has changed, so your                    // componenet is re-rendered 

Redux uses this approach for performance reasons. It takes a really long time to loop through a big state tree looking to see if everything is the same. When you keep your tree immutable, only a === test is needed and the process gets much easier.

like image 154
Robbie Wxyz Avatar answered Oct 14 '22 19:10

Robbie Wxyz