By Nicolas


2016-09-15 14:31:10 8 Comments

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>
        )
    }

};

1 comments

@Robbie Wxyz 2016-09-15 16:53:25

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.

@Nicolas 2016-09-19 14:01:49

Thanks, that was the issue! I was aware of the need of mutation avoidance, nevertheless I thought that the way I was implementing it was a immutable way. Using Object.assign would not return a new item to the state? For me, I did not matter the splice on the past state, since the function was just returning a new state, that despite mutated, was going to return a new state with the Object.assign method. Am I missing something?

@Robbie Wxyz 2016-09-19 17:09:19

@Nicolas Object.assign () only does a shallow copy. So state gets copied, but the notifications array gets mutated.

Related Questions

Sponsored Content

8 Answered Questions

[SOLVED] Why use Redux over Facebook Flux?

0 Answered Questions

Using props state in another state with redux

2 Answered Questions

[SOLVED] Create modal in react-redux using stateless functional components

1 Answered Questions

2 Answered Questions

1 Answered Questions

[SOLVED] React/Redux store updating but Component not re-rendering

1 Answered Questions

2 Answered Questions

[SOLVED] React-redux tutorial : Where does children come from

1 Answered Questions

[SOLVED] Where is best place to load data from redux-store ?

Sponsored Content