Reactステートレスコンポーネントのイベントハンドラー


84

Reactステートレスコンポーネントでイベントハンドラーを作成するための最適な方法を見つけようとしています。私はこのようなことをすることができます:

const myComponent = (props) => {
    const myHandler = (e) => props.dispatch(something());
    return (
        <button onClick={myHandler}>Click Me</button>
    );
}

ここでの欠点は、このコンポーネントがレンダリングされるたびに、新しい「myHandler」関数が作成されることです。コンポーネントのプロパティにアクセスできるステートレスコンポーネントでイベントハンドラーを作成するためのより良い方法はありますか?


useCallback-const memoizedCallback = useCallback(()=> {doSomething(a、b);}、[a、b]、); メモ化されたコールバックを返します。
Shaik Md NRasool20年

回答:


62

関数コンポーネントの要素にハンドラーを適用すると、通常は次のようになります。

const f = props => <button onClick={props.onClick}></button>

もっと複雑なことをする必要がある場合は、a)コンポーネントをステートレスにしない(クラスまたはフックを使用する)か、b)外部のステートフルコンテナコンポーネントにハンドラーを作成する必要があることを示しています。

余談ですが、コンポーネントがアプリの特に集中的に再レン​​ダリングされた部分にない限り、私の最初のポイントをわずかに損なうことはありませんrender()。で矢印関数を作成することを心配する必要はありません。


2
これにより、ステートレスコンポーネントがレンダリングされるたびに関数が作成されるのをどのように回避できますか?
zero_cool

1
上記のコード例は、参照によって適用されているハンドラーを示しているだけであり、そのコンポーネントのレンダリング時に新しいハンドラー関数は作成されません。外部コンポーネントがuseCallback(() => {}, [])orを使用してハンドラーを作成した場合、this.onClick = this.onClick.bind(this)コンポーネントはレンダリングごとに同じハンドラー参照を取得するため、React.memoorの使用に役立ちますshouldComponentUpdate(ただし、これは集中的に再レン​​ダリングされた多数の/複雑なコンポーネントにのみ関連します)。
JedRichards19年

48

新しいReactフック機能を使用すると、次のようになります。

const HelloWorld = ({ dispatch }) => {
  const handleClick = useCallback(() => {
    dispatch(something())
  })
  return <button onClick={handleClick} />
}

useCallback メモ化された関数を作成します。つまり、レンダリングサイクルごとに新しい関数が再生成されることはありません。

https://reactjs.org/docs/hooks-reference.html#usecallback

ただし、これはまだ提案段階です。


7
ReactフックはReact16.8でリリースされ、現在Reactの公式の一部となっています。したがって、この答えは完全に機能します。
cutemachine

3
eslint-plugin-react-hooksパッケージの一部として推奨されるexhaustive-depsルールには、「React Hook useCallbackは、1つの引数だけで呼び出された場合は何もしません」と書かれていることに注意してください。したがって、この場合、空の配列は次のようになります。 2番目の引数として渡されます。
olegzhermal

1
上記の例では、useCallback-を使用しても効率は向上しません。また、レンダリングごとに新しい矢印関数を生成しています(引数はに渡されますuseCallback)。useCallback不要なレンダリングを防ぐために参照の同等性に依存する最適化された子コンポーネントにコールバックを渡す場合にのみ役立ちます。ボタンのようなHTML要素にコールバックを適用するだけの場合は、を使用しないでくださいuseCallback
JedRichards19年

1
@JedRichards新しい矢印機能は、各レンダリング上で作成されているが、DOMは、時間を節約する必要のある更新する必要はありません
ハーマン

3
@herman(わずかなパフォーマンスのペナルティを除いて)まったく違いはありません。そのため、コメントしているこの回答は少し疑わしいです:)依存関係配列を持たないフックは、更新のたびに実行されます(説明されています) useEffectドキュメントの開始近く)。私が述べたように、集中的/高価に再レンダリングされる子コンポーネントに渡すことを計画しているコールバック関数への安定した/メモ化された参照が必要な場合にのみ、useCallbackを使用する必要があります。参照の同等性は重要です。その他の使用法は、毎回レンダリングで新しい関数を作成するだけです。
JedRichards19年

16

この方法はどうですか:

const myHandler = (e,props) => props.dispatch(something());

const myComponent = (props) => {
 return (
    <button onClick={(e) => myHandler(e,props)}>Click Me</button>
  );
}

15
良い考えです!残念ながら、これでは、レンダリングを呼び出すたびに新しい関数を作成するという問題を回避できません。github.com
yannickcr

@aStewartDesignこの問題の解決策または更新はありますか?私は同じ問題に直面しているので、それを聞いて本当にうれしいです
キム

4
myHandlerの実装を持つ親の通常のコンポーネントがあり、それをサブコンポーネントに渡すだけです
Raja Rao

私はこれまでのところ(2018年7月)これより良い方法はないと思います、誰かが何かクールなものを見つけた場合、plsは私に知らせます
a_m_dev 2018

どうして<button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>?つまり、myHandler関数でラップしないでください。
SimonFranzen18年

6

ハンドラーが変更されるプロパティに依存している場合、ハンドラーをキャッシュするステートフルインスタンスがないため、ハンドラーを毎回作成する必要があります。動作する可能性のある別の代替方法は、入力小道具に基づいてハンドラーをメモ化することです。

カップルの実装オプションは lodash._memoize R.memoize 高速memoizeを


4

ソリューション1のmapPropsToHandlerとevent.target。

関数はjsのオブジェクトであるため、プロパティをアタッチすることができます。

function onChange() { console.log(onChange.list) }

function Input(props) {
    onChange.list = props.list;
    return <input onChange={onChange}/>
}

この関数は、プロパティを関数に1回だけバインドします。

export function mapPropsToHandler(handler, props) {
    for (let property in props) {
        if (props.hasOwnProperty(property)) {
            if(!handler.hasOwnProperty(property)) {
                 handler[property] = props[property];
            }
        }
    }
}

私は私の小道具をこのように手に入れます。

export function InputCell({query_name, search, loader}) {
    mapPropsToHandler(onChange, {list, query_name, search, loader});
    return (
       <input onChange={onChange}/> 
    );
}

function onChange() {
    let {query_name, search, loader} = onChange;
    
    console.log(search)
}

この例では、event.targetとmapPropsToHandlerの両方を組み合わせています。数値や文字列だけでなく、ハンドラーに関数をアタッチすることをお勧めします。数値と文字列は、次のようなDOM属性を使用して渡すことができます。

<select data-id={id}/>

mapPropsToHandlerではなく

import React, {PropTypes} from "react";
import swagger from "../../../swagger/index";
import {sync} from "../../../functions/sync";
import {getToken} from "../../../redux/helpers";
import {mapPropsToHandler} from "../../../functions/mapPropsToHandler";

function edit(event) {
    let {translator} = edit;
    const id = event.target.attributes.getNamedItem('data-id').value;
    sync(function*() {
        yield (new swagger.BillingApi())
            .billingListStatusIdPut(id, getToken(), {
                payloadData: {"admin_status": translator(event.target.value)}
            });
    });
}

export default function ChangeBillingStatus({translator, status, id}) {
    mapPropsToHandler(edit, {translator});

    return (
        <select key={Math.random()} className="form-control input-sm" name="status" defaultValue={status}
                onChange={edit} data-id={id}>
            <option data-tokens="accepted" value="accepted">{translator('accepted')}</option>
            <option data-tokens="pending" value="pending">{translator('pending')}</option>
            <option data-tokens="rejected" value="rejected">{translator('rejected')}</option>
        </select>
    )
}

解決策2。イベントの委任

解決策1を参照してください。入力からイベントハンドラーを削除して、他の入力も保持する親に配置できます。ヘルプ委任手法により、event.traget関数とmapPropsToHandler関数を再度使用できます。


悪い習慣!関数はその目的を果たすだけであり、プロパティを保持しないようにいくつかのパラメーターでロジックを実行することを目的としています。JavaScriptが同じことを行うための多くの創造的な方法を許可しているからといって、自分で機能するものを使用できるようにする必要はありません。
BeyondTheSea

4

これは、typescriptでreactとreduxを記述して実装された私の簡単なお気に入りの製品リストです。カスタムハンドラーで必要なすべての引数を渡し、EventHandlerオリジンイベント引数を受け入れる新しい引数を返すことができます。それはだMouseEvent、この例では。

分離された関数はjsxをよりクリーンに保ち、いくつかのリンティングルールに違反するのを防ぎます。以下のようなjsx-no-bindjsx-no-lambda

import * as React from 'react';
import { DispatchProp, Dispatch, connect } from 'react-redux';
import { removeFavorite } from './../../actions/favorite';

interface ListItemProps {
  prod: Product;
  handleRemoveFavoriteClick: React.EventHandler<React.MouseEvent<HTMLButtonElement>>;
}

const ListItem: React.StatelessComponent<ListItemProps> = (props) => {
  const {
    prod,
    handleRemoveFavoriteClick
  } = props;  

  return (
    <li>
      <a href={prod.url} target="_blank">
        {prod.title}
      </a>
      <button type="button" onClick={handleRemoveFavoriteClick}>&times;</button>
    </li>
  );
};

const handleRemoveFavoriteClick = (prod: Product, dispatch: Dispatch<any>) =>
  (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();

    dispatch(removeFavorite(prod));
  };

interface FavoriteListProps {
  prods: Product[];
}

const FavoriteList: React.StatelessComponent<FavoriteListProps & DispatchProp<any>> = (props) => {
  const {
    prods,
    dispatch
  } = props;

  return (
    <ul>
      {prods.map((prod, index) => <ListItem prod={prod} key={index} handleRemoveFavoriteClick={handleRemoveFavoriteClick(prod, dispatch)} />)}
    </ul>    
  );
};

export default connect()(FavoriteList);

typescriptに慣れていない場合は、javascriptスニペットを次に示します。

import * as React from 'react';
import { DispatchProp, Dispatch, connect } from 'react-redux';
import { removeFavorite } from './../../actions/favorite';

const ListItem = (props) => {
  const {
    prod,
    handleRemoveFavoriteClick
  } = props;  

  return (
    <li>
      <a href={prod.url} target="_blank">
        {prod.title}
      </a>
      <button type="button" onClick={handleRemoveFavoriteClick}>&times;</button>
    </li>
  );
};

const handleRemoveFavoriteClick = (prod, dispatch) =>
  (e) => {
    e.preventDefault();

    dispatch(removeFavorite(prod));
  };

const FavoriteList = (props) => {
  const {
    prods,
    dispatch
  } = props;

  return (
    <ul>
      {prods.map((prod, index) => <ListItem prod={prod} key={index} handleRemoveFavoriteClick={handleRemoveFavoriteClick(prod, dispatch)} />)}
    </ul>    
  );
};

export default connect()(FavoriteList);

2

ステートレスコンポーネントの場合と同様に、関数を追加するだけです-

function addName(){
   console.log("name is added")
}

そしてそれはリターンで呼び出されます onChange={addName}


1

あなたが心配している小道具にいくつかの機能しかない場合は、これを行うことができます:

let _dispatch = () => {};

const myHandler = (e) => _dispatch(something());

const myComponent = (props) => {
    if (!_dispatch)
        _dispatch = props.dispatch;

    return (
        <button onClick={myHandler}>Click Me</button>
    );
}

それがはるかに複雑になった場合、私は通常、クラスコンポーネントを持つことに戻ります。


1

継続的な努力の後、ようやく私のために働きました。

//..src/components/atoms/TestForm/index.tsx

import * as React from 'react';

export interface TestProps {
    name?: string;
}

export interface TestFormProps {
    model: TestProps;
    inputTextType?:string;
    errorCommon?: string;
    onInputTextChange: React.ChangeEventHandler<HTMLInputElement>;
    onInputButtonClick: React.MouseEventHandler<HTMLInputElement>;
    onButtonClick: React.MouseEventHandler<HTMLButtonElement>;
}

export const TestForm: React.SFC<TestFormProps> = (props) => {    
    const {model, inputTextType, onInputTextChange, onInputButtonClick, onButtonClick, errorCommon} = props;

    return (
        <div>
            <form>
                <table>
                    <tr>
                        <td>
                            <div className="alert alert-danger">{errorCommon}</div>
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <input
                                name="name"
                                type={inputTextType}
                                className="form-control"
                                value={model.name}
                                onChange={onInputTextChange}/>
                        </td>
                    </tr>                    
                    <tr>
                        <td>                            
                            <input
                                type="button"
                                className="form-control"
                                value="Input Button Click"
                                onClick={onInputButtonClick} />                            
                        </td>
                    </tr>
                    <tr>
                        <td>
                            <button
                                type="submit"
                                value='Click'
                                className="btn btn-primary"
                                onClick={onButtonClick}>
                                Button Click
                            </button>                            
                        </td>
                    </tr>
                </table>
            </form>
        </div>        
    );    
}

TestForm.defaultProps ={
    inputTextType: "text"
}

//========================================================//

//..src/components/atoms/index.tsx

export * from './TestForm';

//========================================================//

//../src/components/testpage/index.tsx

import * as React from 'react';
import { TestForm, TestProps } from '@c2/component-library';

export default class extends React.Component<{}, {model: TestProps, errorCommon: string}> {
    state = {
                model: {
                    name: ""
                },
                errorCommon: ""             
            };

    onInputTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const field = event.target.name;
        const model = this.state.model;
        model[field] = event.target.value;

        return this.setState({model: model});
    };

    onInputButtonClick = (event: React.MouseEvent<HTMLInputElement>) => {
        event.preventDefault();

        if(this.validation())
        {
            alert("Hello "+ this.state.model.name + " from InputButtonClick.");
        }
    };

    onButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
        event.preventDefault();

        if(this.validation())
        {
            alert("Hello "+ this.state.model.name+ " from ButtonClick.");
        }
    };

    validation = () => {
        this.setState({ 
            errorCommon: ""
        });

        var errorCommonMsg = "";
        if(!this.state.model.name || !this.state.model.name.length) {
            errorCommonMsg+= "Name: *";
        }

        if(errorCommonMsg.length){
            this.setState({ errorCommon: errorCommonMsg });        
            return false;
        }

        return true;
    };

    render() {
        return (
            <TestForm model={this.state.model}  
                        onInputTextChange={this.onInputTextChange}
                        onInputButtonClick={this.onInputButtonClick}
                        onButtonClick={this.onButtonClick}                
                        errorCommon={this.state.errorCommon} />
        );
    }
}

//========================================================//

//../src/components/home2/index.tsx

import * as React from 'react';
import TestPage from '../TestPage/index';

export const Home2: React.SFC = () => (
  <div>
    <h1>Home Page Test</h1>
    <TestPage />
  </div>
);

注:テキストボックスにファイルされたバインディングの場合、「name」属性と「property name」(例:model.name)は同じである必要があり、「onInputTextChange」のみが機能します。「onInputTextChange」ロジックは、コードによって変更できます。


0

このようなものはどうですか:

let __memo = null;
const myHandler = props => {
  if (!__memo) __memo = e => props.dispatch(something());
  return __memo;
}

const myComponent = props => {
  return (
    <button onClick={myHandler(props)}>Click Me</button>
  );
}

しかし、例のように、onClickを下位/内部コンポーネントに渡す必要がない場合、これは実際にはやり過ぎです。

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