How to unmount, unrender or remove a component, from itself in a React/Redux/Typescript notification message
I know this question has been asked a couple of times already but most of the time, the solution is to handle this in the parent, as the flow of responsibility is only descending. However, sometimes, you need to kill a component from one of its methods. I know I can't modify its props, and If I start adding booleans as the state, it's gonna start to be really messy for a simple component. Here is what I'm trying to achieve : A small error box component, with an "x" to dismiss it. Receiving an error through its props will display it but I'd like a way to close it from its own code.
class ErrorBoxComponent extends React.Component {
dismiss() {
// What should I put here?
}
render() {
if (!this.props.error) {
return null;
}
return (
<div data-alert className="alert-box error-box">
{this.props.error}
<a href="#" className="close" onClick={this.dismiss.bind(this)}>×</a>
</div>
);
}
}
export default ErrorBoxComponent;
And I would use it like this in the parent component :
<ErrorBox error={this.state.error}/>
In the section What should I put here ?, I already tried :
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);
Which throws a nice error in the console :
Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by React and is not a top-level container. Instead, have the parent component update its state and rerender in order to remove this component.
Should I copy the incoming props in the ErrorBox state, and manipulate it only internally?
Just like that nice warning you got, you are trying to do something that is an Anti-Pattern in React. This is a no-no. React is intended to have an unmount happen from a parent to child relationship. Now if you want a child to unmount itself, you can simulate this with a state change in the parent that is triggered by the child. let me show you in code.
class Child extends React.Component {
constructor(){}
dismiss() {
this.props.unmountMe();
}
render(){
// code
}
}
class Parent ...
constructor(){
super(props)
this.state = {renderChild: true};
this.handleChildUnmount = this.handleChildUnmount.bind(this);
}
handleChildUnmount(){
this.setState({renderChild: false});
}
render(){
// code
{this.state.renderChild ? <Child unmountMe={this.handleChildUnmount} /> : null}
}
}
this is a very simple example. but you can see a rough way to pass through to the parent an action
That being said you should probably be going through the store (dispatch action) to allow your store to contain the correct data when it goes to render
I've done error/status messages for two separate applications, both went through the store. It's the preferred method... If you'd like I can post some code as to how to do that.
EDIT: Here is how I set up a notification system using React/Redux/Typescript
Few things to note first. this is in typescript so you would need to remove the type declarations :)
I am using the npm packages lodash for operations, and classnames (cx alias) for inline classname assignment.
The beauty of this setup is I use a unique identifier for each notification when the action creates it. (e.g. notify_id). This unique ID is a Symbol()
. This way if you want to remove any notification at any point in time you can because you know which one to remove. This notification system will let you stack as many as you want and they will go away when the animation is completed. I am hooking into the animation event and when it finishes I trigger some code to remove the notification. I also set up a fallback timeout to remove the notification just in case the animation callback doesn't fire.
notification-actions.ts
import { USER_SYSTEM_NOTIFICATION } from '../constants/action-types';
interface IDispatchType {
type: string;
payload?: any;
remove?: Symbol;
}
export const notifySuccess = (message: any, duration?: number) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: true, message, notify_id: Symbol(), duration } } as IDispatchType);
};
};
export const notifyFailure = (message: any, duration?: number) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, payload: { isSuccess: false, message, notify_id: Symbol(), duration } } as IDispatchType);
};
};
export const clearNotification = (notifyId: Symbol) => {
return (dispatch: Function) => {
dispatch({ type: USER_SYSTEM_NOTIFICATION, remove: notifyId } as IDispatchType);
};
};
notification-reducer.ts
const defaultState = {
userNotifications: []
};
export default (state: ISystemNotificationReducer = defaultState, action: IDispatchType) => {
switch (action.type) {
case USER_SYSTEM_NOTIFICATION:
const list: ISystemNotification[] = _.clone(state.userNotifications) || [];
if (_.has(action, 'remove')) {
const key = parseInt(_.findKey(list, (n: ISystemNotification) => n.notify_id === action.remove));
if (key) {
// mutate list and remove the specified item
list.splice(key, 1);
}
} else {
list.push(action.payload);
}
return _.assign({}, state, { userNotifications: list });
}
return state;
};
app.tsx
in the base render for your application you would render the notifications
render() {
const { systemNotifications } = this.props;
return (
<div>
<AppHeader />
<div className="user-notify-wrap">
{ _.get(systemNotifications, 'userNotifications') && Boolean(_.get(systemNotifications, 'userNotifications.length'))
? _.reverse(_.map(_.get(systemNotifications, 'userNotifications', []), (n, i) => <UserNotification key={i} data={n} clearNotification={this.props.actions.clearNotification} />))
: null
}
</div>
<div className="content">
{this.props.children}
</div>
</div>
);
}
user-notification.tsx
user notification class
/*
Simple notification class.
Usage:
<SomeComponent notifySuccess={this.props.notifySuccess} notifyFailure={this.props.notifyFailure} />
these two functions are actions and should be props when the component is connect()ed
call it with either a string or components. optional param of how long to display it (defaults to 5 seconds)
this.props.notifySuccess('it Works!!!', 2);
this.props.notifySuccess(<SomeComponentHere />, 15);
this.props.notifyFailure(<div>You dun goofed</div>);
*/
interface IUserNotifyProps {
data: any;
clearNotification(notifyID: symbol): any;
}
export default class UserNotify extends React.Component<IUserNotifyProps, {}> {
public notifyRef = null;
private timeout = null;
componentDidMount() {
const duration: number = _.get(this.props, 'data.duration', '');
this.notifyRef.style.animationDuration = duration ? `${duration}s` : '5s';
// fallback incase the animation event doesn't fire
const timeoutDuration = (duration * 1000) + 500;
this.timeout = setTimeout(() => {
this.notifyRef.classList.add('hidden');
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}, timeoutDuration);
TransitionEvents.addEndEventListener(
this.notifyRef,
this.onAmimationComplete
);
}
componentWillUnmount() {
clearTimeout(this.timeout);
TransitionEvents.removeEndEventListener(
this.notifyRef,
this.onAmimationComplete
);
}
onAmimationComplete = (e) => {
if (_.get(e, 'animationName') === 'fadeInAndOut') {
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}
}
handleCloseClick = (e) => {
e.preventDefault();
this.props.clearNotification(_.get(this.props, 'data.notify_id') as symbol);
}
assignNotifyRef = target => this.notifyRef = target;
render() {
const {data, clearNotification} = this.props;
return (
<div ref={this.assignNotifyRef} className={cx('user-notification fade-in-out', {success: data.isSuccess, failure: !data.isSuccess})}>
{!_.isString(data.message) ? data.message : <h3>{data.message}</h3>}
<div className="close-message" onClick={this.handleCloseClick}>+</div>
</div>
);
}
}
instead of using
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);
try using
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
In most cases, it is enough just to hide the element, for example in this way:
export default class ErrorBoxComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isHidden: false
}
}
dismiss() {
this.setState({
isHidden: true
})
}
render() {
if (!this.props.error) {
return null;
}
return (
<div data-alert className={ "alert-box error-box " + (this.state.isHidden ? 'DISPLAY-NONE-CLASS' : '') }>
{ this.props.error }
<a href="#" className="close" onClick={ this.dismiss.bind(this) }>×</a>
</div>
);
}
}
Or you may render/rerender/not render via parent component like this
export default class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isErrorShown: true
}
}
dismiss() {
this.setState({
isErrorShown: false
})
}
showError() {
if (this.state.isErrorShown) {
return <ErrorBox
error={ this.state.error }
dismiss={ this.dismiss.bind(this) }
/>
}
return null;
}
render() {
return (
<div>
{ this.showError() }
</div>
);
}
}
export default class ErrorBoxComponent extends React.Component {
dismiss() {
this.props.dismiss();
}
render() {
if (!this.props.error) {
return null;
}
return (
<div data-alert className="alert-box error-box">
{ this.props.error }
<a href="#" className="close" onClick={ this.dismiss.bind(this) }>×</a>
</div>
);
}
}
Finally, there is a way to remove html node, but i really dont know is it a good idea. Maybe someone who knows React from internal will say something about this.
export default class ErrorBoxComponent extends React.Component {
dismiss() {
this.el.remove();
}
render() {
if (!this.props.error) {
return null;
}
return (
<div data-alert className="alert-box error-box" ref={ (el) => { this.el = el} }>
{ this.props.error }
<a href="#" className="close" onClick={ this.dismiss.bind(this) }>×</a>
</div>
);
}
}
I've been to this post about 10 times now and I just wanted to leave my two cents here. You can just unmount it conditionally.
if (renderMyComponent) {
<MyComponent props={...} />
}
All you have to do is remove it from the DOM in order to unmount it.
As long as renderMyComponent = true
, the component will render. If you set renderMyComponent = false
, it will unmount from the DOM.