166

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)}>&times;</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?

Andhi Irawan
  • 456
  • 8
  • 15
Sephy
  • 50,022
  • 30
  • 123
  • 131
  • Are you using Redux? – Arnau Lacambra May 02 '16 at 15:14
  • 1
    Why is this a requirement "Receiving an error through its props will display it but I'd like a way to close it from its own code."? The normal approach would be to dispatch an action that would clear the error state and then get closed in a render cycle of the parent as you alluded to. – ken4z May 02 '16 at 16:43
  • I'd like to offer the possibility for both actually. Indeed, it will be closable as you explained it, but my case is "what if I also want to be able to close it from the inside" – Sephy May 02 '16 at 16:48

5 Answers5

130

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>
        );
    }
}
Andrea Carraro
  • 9,731
  • 5
  • 33
  • 57
John Ruddell
  • 25,283
  • 6
  • 57
  • 86
  • 2
    "through the store" ? I think, Im missing a few crucial lessons about that :D Thanks for the answer and the code but dont you think this is seriously overkill for a simple error message display component ? It should not be the responsibility of the parent to handle an action defined on the child... – Sephy May 02 '16 at 17:08
  • It should be the parent actually since the parent is responsible for putting the child in the DOM in the first place. Like I was saying though, even though this is a way to do it I wouldn't recommend it. You should be using an action that updates your store. both Flux and Redux patterns should be used this way. – John Ruddell May 02 '16 at 17:10
  • Ok then, Id be happy to get some pointer of code fragments If you please. Ill get back to that piece of code when I've read a bit about both Flux and Reduc ! – Sephy May 02 '16 at 17:19
  • Ok yea I think I'll make a simple github repo showing a way to do it. The last one I did I used css animations to fade in fade out the element that could render string or html elements and then when the animation completed I used javascript to listen for that and then clean itself up (remove from the DOM) when either the animation finished or you clicked the dismiss button. – John Ruddell May 02 '16 at 17:21
  • Please do, If it can help others like me who struggle a bit to grasp React's philosophy. Also, I'd glad to part with a bit of my points for the time taken If you do put up a git repo for this ! Lets say a hundred points (bounty available in 2 days though) – Sephy May 02 '16 at 18:56
  • @Sephy that would be very nice of you. if you want to do that feel free to (i wont complain haha). I was planning on doing this already so I'll def get it out there might be a few days though. So if you can hold off till then that would be great. – John Ruddell May 02 '16 at 19:31
  • no pb with waiting, this is not a key part of my app, it can wait a couple days. – Sephy May 03 '16 at 06:51
  • @Sephy are you using an architecture like redux or rlux? – John Ruddell May 03 '16 at 08:03
  • Not yet. I still need to have a look into them. Im quite new to all that, so Im discovering the whole ecosystem ! – Sephy May 03 '16 at 08:07
  • Any luck wit putting up a small repo ? – Sephy May 20 '16 at 05:24
  • @Sephy ah shoot sorry I started a new job and completely forgot. let me see if I can throw something together real quick. – John Ruddell May 25 '16 at 17:11
  • @Sephy I am a terrible person! I'm sorry I never updated this with the system. I just went ahead and added the code. I figure you already did something for your system, but incase you want it for a reference or if anyone else wants to take note from it. Sorry I didn't get around to this :( – John Ruddell May 23 '17 at 20:01
  • How do you then reset the "this.state.renderChild" to be able to mount the child again? Assuming this parent is the topmost parent. – dreamfly Apr 25 '18 at 00:37
  • @dreamfly sorry not sure what you mean, if you want the parent to render the child then you have to have some way to do that aka a button click or something that the parent is rendering – John Ruddell Apr 25 '18 at 01:08
  • @JohnRuddell I'm currently trying to solve an issue within `react-native-share-extension` where the extension crashes when attempting to open it a second time. I suspect this is due to the app not being properly unmounted on the JS thread. So when the native module attempts to mount the app the second time, it is already mounted and crashes. The third attempt works fine, since the app has since crashed. Any advice on how to test this theory? Here's the GitHub thread, if you have a moment. Thanks. https://github.com/alinz/react-native-share-extension/issues/100#issuecomment-419551772 – AndrewHenderson Oct 18 '18 at 19:39
  • Hey there @AndrewHenderson! This sounds like a pretty specific issue that I would probably need to debug to figure out. That aside, few things. 1. this package takes a good amount of time to setup without an `rnpm` option. I would 100X check the implementation on the Java side to make sure its correctly setup. 2. [The author recommends using `react-native-modalbox` to show/hide](https://github.com/alinz/react-native-share-extension#final-note). Did u try that? Maybe the way the modal mounts/unmounts fixes the issue? – John Ruddell Oct 18 '18 at 20:24
  • @JohnRuddell I'm using the native modal module in React Native. I have had the extension working for months. It often breaks with iOS updates and debugging is a challenge. I find that there are initialization issues where the app fails to mount, but the screen is blocked by the Native Share Extension, so the user is forced to hard close the app. I wish debugging in extensions was better. – AndrewHenderson Oct 19 '18 at 00:47
  • Gotcha, I haven't debugged a native component before, but it can't be that hard. Sometime in the next few weeks I'm going to be making one so I can let u know what I find out then :) – John Ruddell Oct 19 '18 at 02:23
37

instead of using

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode);

try using

ReactDOM.unmountComponentAtNode(document.getElementById('root'));
M Rezvani
  • 381
  • 3
  • 4
  • Has anyone tried this with React 15? This seems both potentially-useful and possibly an anti-pattern. – theUtherSide Jun 26 '18 at 00:44
  • 6
    @theUtherSide this is an anti pattern in react. React docs recommend you unmount a child from the parent via state / props – John Ruddell Jun 26 '18 at 07:35
  • 1
    What if the component being unmounted is the root of your React app but not the root element being replaced? For example `
    `. What if the inner text of `c1` gets replaced?
    – flipdoubt Jan 03 '19 at 21:34
  • 3
    This is useful if you want to unmount your root component especially if you have a react app residing in a non-react app. I had to use this because I wanted to render react inside a modal handled by another app, and their modal have close buttons that will hide the modal but my reactdom will still remain mounted. https://reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html – Abba Mar 08 '19 at 12:15
  • 4
    The irony of every other thing in the universe one day finding itself to be a React 'anti-pattern' is that React development is, itself, becoming an anti-pattern. When 80% of your code is process/boilerplate to protect the other 20% from future developers, something is wrong. It's my favourite framework for developing web apps, but it's just a tool, not an orthodoxy. – MickMalone1983 Jan 02 '21 at 18:57
  • Maybe I should have been a little more clear in my comment. For the OP's use case it's an anti-pattern. The api (`unmountComponentAtNode`) exists for a reason and is useful for some use cases, but removing a toast notification isn't a good use case for this api. – John Ruddell May 15 '22 at 05:12
13

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) }>&times;</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) }>&times;</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) }>&times;</a>
            </div>
        );
    }
}
Sasha Kos
  • 2,480
  • 2
  • 22
  • 37
  • But, in the case that I want to unmount a child that is inside a list of children... What can I do if I want to replace a cloned component with the same key in that list? – roadev Sep 30 '17 at 00:30
  • 1
    as i understand you want to do something like this: document.getElementById( CHILD_NODE_ID ) -> .remove(); -> document.getElementById( PARENT_NODE_ID) -> .appendChild(NEW_NODE)? Am i right? Forget about it. It is NOT react approach. Use component state for condition rendering – Sasha Kos Apr 12 '18 at 16:20
9

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.

ihodonald
  • 745
  • 1
  • 12
  • 27
-1

This isn't appropriate in all situations but you can conditionally return false inside the component itself if a certain criteria is or isn't met.

It doesn't unmount the component, but it removes all rendered content. This would only be bad, in my mind, if you have event listeners in the component that should be removed when the component is no longer needed.

import React, { Component } from 'react';

export default class MyComponent extends Component {
    constructor(props) {
        super(props);

        this.state = {
            hideComponent: false
        }
    }

    closeThis = () => {
        this.setState(prevState => ({
            hideComponent: !prevState.hideComponent
        })
    });

    render() {
        if (this.state.hideComponent === true) {return false;}

        return (
            <div className={`content`} onClick={() => this.closeThis}>
                YOUR CODE HERE
            </div>
        );
    }
}
nebulousecho
  • 326
  • 1
  • 2
  • 9