React / Redux / Typescript通知メッセージで、コンポーネントをそれ自体からマウント解除、レンダリング解除、または削除する方法


114

私はこの質問がすでに何度か尋ねられていることを知っていますが、ほとんどの場合、解決策は親でこれを処理することです。責任の流れが下降しているだけだからです。ただし、コンポーネントをそのメソッドの1つから強制終了する必要がある場合があります。その小道具を変更できないことはわかっています。状態としてブール値を追加し始めると、単純なコンポーネントの場合は本当に面倒になります。これが私が達成しようとしていることです:小さなエラーボックスコンポーネントで、それを閉じるための「x」があります。小道具を介してエラーを受け取ると表示されますが、独自のコードからそれを閉じる方法が欲しいのですが。

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;

そして、私はそれを親コンポーネントで次のように使用します:

<ErrorBox error={this.state.error}/>

セクションでは 、私はここで何を置くべきですか?、私はすでに試しました:

ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).parentNode); これはコンソールに素敵なエラーを投げます:

警告:unmountComponentAtNode():アンマウントしようとしているノードは、Reactによってレンダリングされたものであり、最上位のコンテナではありません。代わりに、このコンポーネントを削除するために、親コンポーネントの状態を更新して再レンダリングしてください。

ErrorBox状態で着信プロップをコピーし、内部でのみ操作する必要がありますか?


Reduxを使用していますか?
Arnau Lacambra 2016年

なぜこれが「小道具を介してエラーを受け取ると表示されますが、独自のコードから閉じる方法が欲しい」という要件なのですか?通常のアプローチは、エラー状態をクリアするアクションをディスパッチしてから、親のレンダーサイクルで閉じた状態になります。
ken4z 2016年

実際に両方の可能性を提供したいと思います。実際、説明どおりに閉じることができますが、私のケースは「内側からも閉じられるようにしたい場合」です
Sephy

回答:


97

あなたが受け取ったその素晴らしい警告のように、あなたはReactのアンチパターンである何かをしようとしています。これはノーノーです。Reactは、アンマウントを親子関係から発生させることを目的としています。子が自分でマウントを解除したい場合は、子によってトリガーされる親の状態変更を使用してこれをシミュレートできます。コードでお見せしましょう。

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}
    }

}

これは非常に単純な例です。しかし、あなたは親にアクションを渡す大まかな方法​​を見ることができます

とは言っても、おそらくストア(ディスパッチアクション)を経由して、レンダリング時に正しいデータがストアに含まれるようにする必要があります。

2つの個別のアプリケーションに対してエラー/ステータスメッセージを送信しましたが、どちらもストアを通過しました。それが推奨される方法です...もしそうしたいのであれば、それを行う方法についていくつかのコードを投稿できます。

編集:React / Redux / Typescriptを使用して通知システムを設定する方法は次のとおりです

最初に注意することはほとんどありません。これはtypescriptなので、型宣言を削除する必要があります:)

操作にはnpmパッケージlodashを使用し、インラインクラス名の割り当てにはクラス名(cxエイリアス)を使用しています。

この設定の利点は、アクションが作成する通知ごとに一意の識別子を使用することです。(例:notify_id)。この一意のIDはSymbol()です。これにより、どの通知をいつでも削除したい場合は、どの通知を削除するかがわかっているので、削除できます。この通知システムでは、好きなだけ積み重ねることができ、アニメーションが完了すると消えます。アニメーションイベントにフックし、それが終了したら、コードをトリガーして通知を削除します。また、アニメーションコールバックが起動しない場合に備えて、通知を削除するためのフォールバックタイムアウトを設定しました。

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

アプリケーションの基本レンダリングでは、通知をレンダリングします

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

ユーザー通知クラス

/*
    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>
        );
    }
}

1
「店を通して」?私はそれについていくつかの重要な教訓を欠いていると思います:D答えとコードをありがとう 子供に定義されたアクションを処理するのは、親の責任ではありません...
Sephy

親は最初にDOMに子を配置する責任があるため、実際には親である必要があります。私が言っていたように、これはそれを行う方法ですが、私はそれをお勧めしません。ストアを更新するアクションを使用する必要があります。FluxパターンとReduxパターンの両方をこの方法で使用する必要があります。
John Ruddell、

それでは、よろしければ、コードフラグメントのポインタを取得してください。FluxとReducの両方について少し読んだら、そのコードに戻ります。
Sephy

わかりました。それを行う方法を示す簡単なgithubリポジトリを作成します。私が最後に行ったのは、CSSアニメーションを使用して、文字列またはHTML要素をレンダリングできる要素をフェードアウトし、アニメーションが完了したら、JavaScriptを使用してそれをリッスンし、それをクリーンアップ(DOMから削除)したときにアニメーションが終了したか、閉じるボタンをクリックした。
John Ruddell、

私のような、Reactの哲学を理解するのに少し苦労している他の人を助けることができるなら、どうぞ。また、このためにgitリポジトリを作成した場合は、少し時間を割いてポイントを手放せて嬉しいです!100ポイントとしましょう(ただし、賞金は2日間で利用可能です)
Sephy

25

使用する代わりに

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

使ってみる

ReactDOM.unmountComponentAtNode(document.getElementById('root'));

誰かがこれをReact 15で試しましたか?これは、潜在的に有用で、おそらくアンチパターンのようです。
theUtherSide

4
@theUtherSideこれは反動パターンです。React docsは、state / propsを介して親から子のマウントを解除することを推奨しています
John Ruddell

1
アンマウントするコンポーネントがReactアプリのルートであり、ルート要素ではない場合はどうなりますか?例えば<div id="c1"><div id="c2"><div id="react-root" /></div></div>。の内部テキストc1が置き換えられた場合はどうなりますか?
フリップダウト

1
これは、ルートコンポーネントをアンマウントする場合、特に、reactアプリがnon-reactアプリにある場合に役立ちます。別のアプリで処理されるモーダル内で反応をレンダリングしたいと思ったため、これを使用する必要がありました。彼らのモーダルには、モーダルを非表示にする閉じるボタンがありますが、私の反応はまだマウントされたままです。reactjs.org/blog/2015/10/01/react-render-and-top-level-api.html
Abba

10

ほとんどの場合、たとえば次のように、要素を非表示にするだけで十分です。

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>
        );
    }
}

または、このような親コンポーネントを介してレンダリング/レンダリング/レンダリングしない可能性があります

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>
        );
    }
}

最後に、htmlノードを削除する方法がありますが、それが良いアイデアかどうかは本当にわかりません。たぶん内部からReactを知っている人がこれについて何か言うでしょう。

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>
        );
    }
}

しかし、子のリスト内にある子のマウントを解除したい場合...複製されたコンポーネントをそのリストの同じキーで置き換えるにはどうすればよいですか?
roadev 2017

1
私はあなたがこのようなことをしたいことを理解しているように:document.getElementById(CHILD_NODE_ID)-> .remove(); -> document.getElementById(PARENT_NODE_ID)-> .appendChild(NEW_NODE)?私は正しいですか?気にしないで。反応アプローチではありません。コンディションレンダリングにコンポーネントの状態を使用
Sasha Kos

2

私はこの記事に10回ほど行ったことがありますが、2セントはここに残したいと思っていました。条件付きでアンマウントするだけです。

if (renderMyComponent) {
  <MyComponent props={...} />
}

マウントを解除するには、DOMから削除するだけです。

である限りrenderMyComponent = true、コンポーネントはレンダリングされます。を設定renderMyComponent = falseすると、DOMからマウント解除されます。


0

これはすべての状況に適しているわけではありませんが、条件付きで return false、特定の基準が満たされている場合、または満たされていない場合は、でコンポーネント自体の内部にする。

コンポーネントはアンマウントされませんが、レンダリングされたコンテンツはすべて削除されます。私の考えでは、コンポーネントが不要になったときに削除する必要があるイベントリスナーがコンポーネントにある場合、これは悪いことです。

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>
        );
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.