igorprado / react-notification-system

A complete and totally customizable component for notifications in React
http://igorprado.github.io/react-notification-system/
MIT License
2.45k stars 249 forks source link

Example in ReadMe should explain how to use context #72

Open nuclearspike opened 8 years ago

nuclearspike commented 8 years ago

Rather than worrying about how to integrate with Redux/Flux/Reflux/etc, I've found the best way to integrate this with a single page app is to use context.

Given this structure in my App container component's render:

<div >
    <Header />
    <NotificationSystem ref={ref => this.notifications = ref} />
    {children}
   <Footer />
</div>

Side note: you can pass a function to ref rather than setting it in a lifecycle method like "componentDidMount" (as shown in the example code).

Then you put this in the App component...

  static childContextTypes = {
    addNotification: PropTypes.func,
  };

  getChildContext() {
    return {
      addNotification: this.addNotification.bind(this),
    };
  }

  addNotification(notification) {
    this.notifications.addNotification(notification);
  }

Then, when any child component that is rendered inside the App component declares a context type like so:

static contextTypes = {
  addNotification: PropTypes.func.isRequired,
};

Then you can call it like this in that child component:

someFunction(){
    this.context.addNotification({
      title: 'New Message',
      message: "You've received a message from the server!",
      level: 'warning',
      dismissable: false,
    });
}

Using context is easier, IMO, than using a redux/reflux/flux because this component was made to have a function called rather than have its state set, which context is much better for.

jamesvclements commented 7 years ago

@nuclearspike I like your use of context and I'm currently trying to implement in the same way in a single page app. However, when trying to call addNotification from the child I get this error:

screen shot 2016-11-15 at 2 11 58 pm

I feel like it is an issue with binding but I'm not sure. Any idea what might be the problem?

I'm trying to implement it here with the src/views/Container.js as the parent component and currently just testing src/views/Landing/Landing.js as one of the children.

nuclearspike commented 7 years ago

in Landing.js you need to declare that you want that context available to the component.

static contextTypes = { addNotification: T.func.isRequired, router: T.object }

I'd also move the "<NotificationSystem " line up to the top in the Container. Are you trying to do the addNotification first thing in a child component (like during mount)?

The "notifications" field of the Container component isn't set until that component is rendered and if other things are rendering first and are trying to make the call to addNotification, then in that container's function the "this.notifications.addNotification()" call fails because "notifications" hasn't been set yet. So, moving it to the top will solve that.

If we'd named the function that gets exposed through the context something different, then it would be more obvious where the failure is.

bhishp commented 7 years ago

Is this the recommended way of providing global notifications for the notif-system in a non-flux app? I know that the context system in React is an experimental API and there are warnings against using it but we see libs like Material-ui and router making using of context.

react-notification-system could provide a <NotificationProvider> (or similar) Component which handles this out of the box.

For example:

Notification Provider

import React, { Component } from 'react';
import { PropTypes } from 'prop-types';

import NotificationSystem from 'react-notification-system';

class NotificationProvider extends Component {

  static propTypes = {
    children: PropTypes.node,
  };

  static defaultProps = {
    children: '',
  };

  static childContextTypes = {
    getNotificationSystem: PropTypes.func,
  };

  getChildContext() {
    return {
      getNotificationSystem: this.getNotificationSystem.bind(this),
    };
  }

  getNotificationSystem() {
    return this.notification;
  }

  render() {
    return (
      <div>
        {this.props.children}
        <NotificationSystem ref={(ref) => { this.notification = ref; }} />
      </div>
    );
  }
}

export default NotificationProvider;

A component somewhere in your app

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class DeepComponent extends Component {

  handleClick(e) {
    e.preventDefault();
    this.context.getNotificationSystem().addNotification({
      message: 'foo',
      level: 'success',
    });
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick.bind(this)}>Button</button>
      </div>
    );
  }
}

DeepComponent.contextTypes = {
  getNotificationSystem: PropTypes.func.isRequired,
};

export default DeepComponent;

App entry point

  render(
      <NotificationProvider>
        <App />
      </NotificationProvider>
    , document.getElementById('site-page-target'));
  });

Note, compares to earlier example that exposes addNotification, here we pass the entire notificationSystem in context, then we can call any of the notification system functions.

Also, had to use a function to return the notification system, rather than passing the notificationSystem itself. I think this is because the context may provided before the provided has mounted (so, this.notificationSystem is undefined at the point of getChildContex).

Beej126 commented 7 years ago

@nuclearspike, thanks for the React Context suggestion ! for anyone coming to this from Visual Studio 2017 react webpack typescript template, i've adapted this to the react-hot-loader AppContainer ...

(new file) NotifierAppContainer.tsx

import * as React from 'react';
import { AppContainer } from 'react-hot-loader';
import * as NotificationSystem from 'react-notification-system';

//adapted to react-hot-loader AppContainer from:https://github.com/igorprado/react-notification-system/issues/72
export class NotifierAppContainer extends AppContainer {
  public notificationSystem: any;

  static childContextTypes = {
    addNotification: React.PropTypes.func,
  };

  getChildContext() {
    return {
      addNotification: this.addNotification.bind(this),
    };
  }

  addNotification(notification: NotificationSystem.Notification) {
    this.notificationSystem.addNotification(notification);
  }

  public render() {
    return <div>
      <NotificationSystem ref={(ref: any) => this.notificationSystem = ref}/>
      {super.render()}
    </div>
  }
}

(updated) boot.tsx

import './css/site.css';
import 'bootstrap';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { BrowserRouter } from 'react-router-dom';
import * as RoutesModule from './routes';
let routes = RoutesModule.routes;
import * as NotificationSystem from 'react-notification-system';
import { NotifierAppContainer } from './NotifierAppContainer'

function renderApp() {
  // This code starts up the React app when it runs in a browser. It sets up the routing
  // configuration and injects the app into a DOM element.
  const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!;
  ReactDOM.render(
    <NotifierAppContainer>
      <div>
        <BrowserRouter children={routes} basename={baseUrl} />
      </div>
    </NotifierAppContainer>,
    document.getElementById('react-app')
  );
}

renderApp();

// Allow Hot Module Replacement
if (module.hot) {
  module.hot.accept('./routes', () => {
    routes = require<typeof RoutesModule>('./routes').routes;
    renderApp();
  });
}

usage in AnyComponent.tsx

export class AnyComponent extends React.Component<RouteComponentProps<{}>, AnyComponentState> {

  static contextTypes = {
    addNotification: React.PropTypes.func.isRequired,
  };

  handleTestClick = () => {
    this.context.addNotification({
      title: 'New Message',
      message: "You've received a message from the server!",
      level: 'warning',
      dismissable: true,
    });
  }

  public render() {
    return <button className="btn" onClick={this.handleTestClick}>click me!</button>
  }
}