Skip to content

Instantly share code, notes, and snippets.

@everdimension

everdimension/Toaster.js

Last active Oct 17, 2018
Embed
What would you like to do?
Sample code showing a "toaster" architecture
import React from 'react';
import { ToasterContainer, Toaster } from '../';
import { Notification } from '../../../Notification';
/** this instance should be imported by those components that need it */
const toaster = new Toaster();
let id = 0;
export function ToasterExample() {
return (
<div>
<button
onClick={() =>
toaster.show({
id: id++,
payload: {
message: 'Block has been moved',
},
})
}
>
show toast
</button>
{/**
* ToasterContainer should probably be rendered closer
* to the root of the app
*/}
<ToasterContainer
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: 400,
}}
toaster={toaster}
renderToast={({ toast, handleClose }) => (
<Notification
message={toast.payload.message}
onClose={() => handleClose(toast)}
/>
)}
/>
</div>
);
}
class Toaster {
constructor() {
this.listeners = [];
this.id = 0;
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
show(toastConfig) {
Object.assign(toastConfig, { timestamp: Date.now() });
if (!toastConfig.id) {
Object.assign(toastConfig, { id: this.id });
this.id += 1;
}
this.listeners.forEach(l => {
l(toastConfig);
});
}
}
export { Toaster };
import React from 'react';
import PropTypes from 'prop-types';
import s from './ToasterContainer.css';
const propTypes = {
toaster: PropTypes.shape({
subscribe: PropTypes.func,
}).isRequired,
renderToast: PropTypes.func.isRequired,
};
class ToasterContainer extends React.Component {
constructor() {
super();
this.state = {};
this.timers = {};
this.handleCloseToast = this.handleCloseToast.bind(this);
}
componentDidMount() {
this.unsubscribe = this.props.toaster.subscribe(toast => {
this.setState({
[toast.id]: toast,
});
clearTimeout(this.timers[toast.id]);
delete this.timers[toast.id];
this.timers[toast.id] = setTimeout(() => {
this.setState({
[toast.id]: null,
});
delete this.timers[toast.id];
}, 5000);
});
}
componentWillUnmount() {
this.unsubscribe();
Object.values(this.timers).forEach(clearTimeout);
}
handleCloseToast(toast) {
this.setState({
[toast.id]: null,
});
delete this.timers[toast.id];
}
render() {
const { renderToast, toaster, ...restProps } = this.props;
/** Render toasts in reverse-chronological order */
/**
* NOTE:
* Order of object values is guaranteed to be the same
* and in the order that the values were added
*/
const displayToasts = Object.values(this.state)
.filter(Boolean)
.sort((a, b) => b.timestamp - a.timestamp);
return (
<div {...restProps}>
<div>
<div>
{displayToasts.map(toast => (
<div key={toast.id} className={s.Item}>
{this.props.renderToast({
toast,
handleClose: this.handleCloseToast,
})}
</div>
))}
</div>
</div>
</div>
);
}
}
ToasterContainer.propTypes = propTypes;
export { ToasterContainer };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment