Blueprint: ES6 React toast

Created on 29 Nov 2016  路  11Comments  路  Source: palantir/blueprint

Hi guys, and thanks for your useful works! I'm not using typescript but ES6 with React.
I'm trying to use Toast component. I want to show a toast when the error state is set to TRUE from Redux. Then, when toast is dismissed, I want to call an action that set my error state to FALSE.
I am doing wrong? Where I can put onDismiss callback to set FALSE my error state?

import React, {Component} from 'react'
import NavigationBar from './components/NavigationBar'
import {Toaster, Position, Intent} from '@blueprintjs/core'
import * as actions from './actions'
import {connect} from 'react-redux'


import './App.css';

class App extends Component {

    render() {
        const toast = Toaster.create({
            position: Position.TOP,
        });

        const errorToast = toast.show({message: this.props.errorMessage, intent: Intent.DANGER});

        return (

            <div>
                <NavigationBar />
                <div className="container padding-top">
                    {(this.props.isError != "") ? {errorToast} : null }
                    {this.props.children}
                    </div>
            </div>
        );
    }
}

function mapStateToProps(state) {
    return {
        isError: state.errors.error,
        errorMessage: state.errors.message
    }
}

export default connect(mapStateToProps, actions)(App);

Now toast appear, but in console I read an error:

Warning: _renderNewRootComponent(): Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate. Check the render method of App.

and error state remain to TRUE and I don't know how to call onDismiss callback.

Thanks in advance!

core question

Most helpful comment

@alearcy Here's how I handle toasts:

REDUCER / ACTIONS FILE:

import { List } from 'immutable';

export const CLEAR_TOASTS = 'ui/CLEAR_TOASTS';
export const ENQUEUE_TOAST = 'ui/ENQUEUE_TOAST';

const initialState = {
  toastQueue: List([]),
};

const reducer = (state = initialState, action = {}) => {
  switch (action.type) {
    default:
      return state;
    case CLEAR_TOASTS:
      return {
        ...state,
        toastQueue: List([]),
      };
    case ENQUEUE_TOAST: {
      const toastQueue = state.toastQueue.push(action.toast);
      return {
        ...state,
        toastQueue,
      };
    }
  }
};

export const clearToasts = () => ({ type: CLEAR_TOASTS });

// toast = { action, iconName, intent, message, onDismiss, timeout }
export const toast = toastObject => (dispatch, getState) => {
  dispatch({
    type: ENQUEUE_TOAST,
    toast: toastObject,
  });
};

export default reducer;

AN APP COMPONENT SHOWING THE TOASTS:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as Blueprint from '@blueprintjs/core';
import { uiActions } from 'actions'; // THE ABOVE FILE

@connect((state, ownProps) => ({
  // STATE
  toastQueue: state.ui.toastQueue,
}), {
  // ACTIONS
  ...uiActions,
})
export default class AppWrapper extends Component {

  componentDidUpdate(prevProps, prevState) {
    if (this.props.toastQueue.size) {
      this.props.toastQueue.forEach((toast) => {
        this.toaster.show(toast);
      });
      this.props.clearToasts();
    }
  }

  refHandlers = {
    toaster: el => (this.toaster = el),
  };

  render() {
    return (
      <div>
        <Blueprint.Toaster position={ Blueprint.Position.TOP_CENTER } ref={ this.refHandlers.toaster } />
        { this.props.children }
      </div>
    );
  }
}

THEN TO FIRE A TOAST:

dispatch(toast({
  iconName: 'credit-card',
  intent: Blueprint.Intent.SUCCESS,
  message: 'Card was added and auto-selected for you.',
}));

All 11 comments

  1. create the Toaster on componentDidMount, not render, since that should only be done once.
  2. call show on the toaster when props change, not on render.

the Toaster API is quite different from all our other React APIs -- it's a completely imperative API that does a lot of things inside Toaster.create and toast.show, so you shouldn't treat it like a JSX element.

Please @adidahiya can you show me little example of code also with onDismiss callback? thank you so much!

@alearcy Here's how I handle toasts:

REDUCER / ACTIONS FILE:

import { List } from 'immutable';

export const CLEAR_TOASTS = 'ui/CLEAR_TOASTS';
export const ENQUEUE_TOAST = 'ui/ENQUEUE_TOAST';

const initialState = {
  toastQueue: List([]),
};

const reducer = (state = initialState, action = {}) => {
  switch (action.type) {
    default:
      return state;
    case CLEAR_TOASTS:
      return {
        ...state,
        toastQueue: List([]),
      };
    case ENQUEUE_TOAST: {
      const toastQueue = state.toastQueue.push(action.toast);
      return {
        ...state,
        toastQueue,
      };
    }
  }
};

export const clearToasts = () => ({ type: CLEAR_TOASTS });

// toast = { action, iconName, intent, message, onDismiss, timeout }
export const toast = toastObject => (dispatch, getState) => {
  dispatch({
    type: ENQUEUE_TOAST,
    toast: toastObject,
  });
};

export default reducer;

AN APP COMPONENT SHOWING THE TOASTS:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as Blueprint from '@blueprintjs/core';
import { uiActions } from 'actions'; // THE ABOVE FILE

@connect((state, ownProps) => ({
  // STATE
  toastQueue: state.ui.toastQueue,
}), {
  // ACTIONS
  ...uiActions,
})
export default class AppWrapper extends Component {

  componentDidUpdate(prevProps, prevState) {
    if (this.props.toastQueue.size) {
      this.props.toastQueue.forEach((toast) => {
        this.toaster.show(toast);
      });
      this.props.clearToasts();
    }
  }

  refHandlers = {
    toaster: el => (this.toaster = el),
  };

  render() {
    return (
      <div>
        <Blueprint.Toaster position={ Blueprint.Position.TOP_CENTER } ref={ this.refHandlers.toaster } />
        { this.props.children }
      </div>
    );
  }
}

THEN TO FIRE A TOAST:

dispatch(toast({
  iconName: 'credit-card',
  intent: Blueprint.Intent.SUCCESS,
  message: 'Card was added and auto-selected for you.',
}));

All works correctly but I'm don't use List from immutable...can I do it in another way?

@alearcy
You can just use a vanilla array instead of immutable List, replacing immutable code above with;

const initialState = {
  toastQueue: [],
};

...

    case ENQUEUE_TOAST: {
      return {
        ...state,
        [...state.toastQueue, action.toast],
      };
    }

and

  componentDidUpdate(prevProps, prevState) {
    if (this.props.toastQueue.length > 0) {
      this.props.toastQueue.forEach((toast) => {
        this.toaster.show(toast);
      });
      this.props.clearToasts();
    }
  }

Worked a treat for me

Yes greg is correct. The issue with using vanilla js arrays as react props is adding/removing elements doesn't trigger a render - since technically it's still the same array. So you can either manually do the shouldComponentUpdate logic or use an immutable List which becomes a totally different List (object reference) every time you add/remove from it.

Yup, vanilla array mutation (e.g. with push) in reducers is a bad thing. Thats why in my suggested changes I did [...state.toastQueue, action.toast] instead of state.toastQueue.push. This will re-allocate a new array each time and updates will be correctly dispatched.

Greg, I tried to create vanilla array in reducer, but if you try, for example, to fire up more toasts at the same time, in componentDidMount for example, you'll see only one toast at time.

Hmm - hard to know what your problem is without seeing code but when I add (for example) three dispatch action calls into a componentDidMount I see three toasts.

Try stepping through your code in the debugger or add some console logging in actions/reducer etc.

@alearcy Everything working now?

Closing this; please use StackOverflow with the blueprintjs tag for support questions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

scottfr picture scottfr  路  3Comments

ghost picture ghost  路  3Comments

sunabozu picture sunabozu  路  3Comments

raiju picture raiju  路  3Comments

adidahiya picture adidahiya  路  3Comments