Ereignishandler in React Stateless Components

82

Der Versuch, einen optimalen Weg zum Erstellen von Ereignishandlern in reaktionslosen Komponenten von React zu finden. Ich könnte so etwas machen:

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

Der Nachteil hierbei ist, dass jedes Mal, wenn diese Komponente gerendert wird, eine neue "myHandler" -Funktion erstellt wird. Gibt es eine bessere Möglichkeit, Ereignishandler in zustandslosen Komponenten zu erstellen, die weiterhin auf die Komponenteneigenschaften zugreifen können?

aStewartDesign
quelle
useCallback - const memoizedCallback = useCallback (() => {doSomething (a, b);}, [a, b],); Gibt einen gespeicherten Rückruf zurück.
Shaik Md N Rasool

Antworten:

61

Das Anwenden von Handlern auf Elemente in Funktionskomponenten sollte im Allgemeinen folgendermaßen aussehen:

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

Wenn Sie etwas viel Komplexeres tun müssen, ist dies ein Zeichen dafür, dass entweder a) die Komponente nicht zustandslos sein sollte (verwenden Sie eine Klasse oder Hooks) oder b) Sie den Handler in einer äußeren Stateful-Container-Komponente erstellen sollten.

Abgesehen davon und wenn Sie meinen ersten Punkt leicht untergraben, müssen Sie sich keine Gedanken über das Erstellen von Pfeilfunktionen machen, es sei denn, die Komponente befindet sich in einem besonders intensiv neu gerenderten Teil der App render().

Jed Richards
quelle
2
Wie wird vermieden, dass bei jedem Rendern der zustandslosen Komponente eine Funktion erstellt wird?
zero_cool
1
Das obige Codebeispiel zeigt nur einen Handler, der als Referenz angewendet wird. Beim Rendern dieser Komponente wird keine neue Handlerfunktion erstellt. Wenn die äußere Komponente den Handler mit useCallback(() => {}, [])oder this.onClick = this.onClick.bind(this)erstellen würde, würde die Komponente bei jedem Rendern dieselbe Handlerreferenz erhalten, was bei der Verwendung von React.memooder hilfreich sein könnte shouldComponentUpdate(dies ist jedoch nur bei intensiv neu gerenderten zahlreichen / komplexen Komponenten relevant).
Jed Richards
46

Mit der neuen Funktion "React Hooks" könnte dies ungefähr so ​​aussehen:

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

useCallback Erstellt eine gespeicherte Funktion, dh, eine neue Funktion wird nicht bei jedem Renderzyklus neu generiert.

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

Dies befindet sich jedoch noch im Vorschlagsstadium.

ryanVincent
quelle
7
React Hooks wurden in React 16.8 veröffentlicht und sind nun offizieller Bestandteil von React. Diese Antwort funktioniert also perfekt.
Cutemachine
3
Nur um zu beachten, dass die empfohlene Regel für erschöpfende Deps als Teil des Pakets eslint-plugin-react-hooks lautet: "React Hook useCallback macht nichts, wenn es mit nur einem Argument aufgerufen wird." In diesem Fall sollte also ein leeres Array sein als zweites Argument übergeben.
olegzhermal
1
In Ihrem obigen Beispiel wird keine Effizienz erzielt useCallback- und Sie generieren immer noch bei jedem Rendern eine neue Pfeilfunktion (das an übergebene Argument useCallback). useCallbackDies ist nur nützlich, wenn Rückrufe an optimierte untergeordnete Komponenten übergeben werden, die auf Referenzgleichheit beruhen, um unnötiges Rendern zu vermeiden. Wenn Sie den Rückruf nur auf ein HTML-Element wie die Schaltfläche anwenden, verwenden Sie ihn nicht useCallback.
Jed Richards
1
@JedRichards Obwohl bei jedem Rendern eine neue Pfeilfunktion erstellt wird, muss das DOM nicht aktualisiert werden, was Zeit sparen sollte
Hermann
3
@herman Es gibt überhaupt keinen Unterschied (abgesehen von einer kleinen Leistungseinbuße), weshalb diese Antwort, unter der wir kommentieren, etwas verdächtig ist :) Jeder Hook, der kein Abhängigkeitsarray hat, wird nach jedem Update ausgeführt (es wird diskutiert) kurz vor dem Start der useEffect-Dokumente). Wie bereits erwähnt, möchten Sie useCallback so gut wie nur verwenden, wenn Sie einen stabilen / gespeicherten Verweis auf eine Rückruffunktion wünschen, die Sie an eine untergeordnete Komponente übergeben möchten, die intensiv / teuer neu gerendert wird, und die referenzielle Gleichheit ist wichtig. Bei jeder anderen Verwendung erstellen Sie jedes Mal eine neue Funktion beim Rendern.
Jed Richards
16

Wie wäre es auf diese Weise:

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

const myComponent = (props) => {
 return (
    <button onClick={(e) => myHandler(e,props)}>Click Me</button>
  );
}
Phi Nguyen
quelle
14
Guter Gedanke! Leider geht es nicht darum, bei jedem
Renderaufruf
@aStewartDesign eine Lösung oder ein Update für dieses Problem? Ich bin wirklich froh, das zu hören, denn ich stehe vor dem gleichen Problem
Kim
4
Haben Sie eine übergeordnete reguläre Komponente, die die Implementierung des myHandler hat, und übergeben Sie sie dann einfach an die Unterkomponente
Raja Rao
Ich denke, es gibt keinen besseren Weg als diesen bis jetzt (Juli 2018). Wenn jemand etwas cooles fand, lass es mich wissen
a_m_dev
warum nicht <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>? Ich meine, wickeln Sie es nicht in eine myHandler-Funk ein.
Simon Franzen
6

Wenn der Handler auf sich ändernde Eigenschaften angewiesen ist, müssen Sie den Handler jedes Mal erstellen, da Ihnen eine statusbehaftete Instanz fehlt, auf der er zwischengespeichert werden kann. Eine andere Alternative, die funktionieren könnte, wäre, den Handler basierend auf den Eingabestützen auswendig zu lernen.

Paar Implementierungsoptionen lodash._memoize R.memoize fast-memoize

ryanjduffy
quelle
4

Lösung ein mapPropsToHandler und event.target.

Funktionen sind Objekte in js, daher ist es möglich, ihnen Eigenschaften zuzuweisen.

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

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

Diese Funktion bindet eine Eigenschaft nur einmal an eine Funktion.

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

Ich bekomme meine Requisiten einfach so.

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

In diesem Beispiel wurden sowohl event.target als auch mapPropsToHandler kombiniert. Es ist besser, Funktionen nur an Handler anzuhängen, nicht an Zahlen oder Zeichenfolgen. Zahl und Zeichenfolgen können mit Hilfe von DOM-Attributen wie übergeben werden

<select data-id={id}/>

eher als 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>
    )
}

Lösung zwei. Veranstaltungsdelegation

siehe Lösung eins. Wir können den Ereignishandler aus der Eingabe entfernen und ihn seinem übergeordneten Element zuweisen, das auch andere Eingaben enthält, und mithilfe der Hilfedelegationstechnik können wir die Funktionen event.traget und mapPropsToHandler erneut verwenden.

Hassan Gilak
quelle
Schlechte Praxis! Eine Funktion sollte nur ihren Zweck erfüllen. Sie soll Logik für einige Parameter ausführen, um keine Eigenschaften zu enthalten. Nur weil Javascript viele kreative Möglichkeiten bietet, dasselbe zu tun, heißt das nicht, dass Sie sich das verwenden lassen sollten, was auch immer funktioniert.
BeyondTheSea
4

Hier ist meine einfache Liste der Lieblingsprodukte, die mit React- und Redux-Schrift in Typoskript implementiert wurde. Sie können alle erforderlichen Argumente im benutzerdefinierten Handler übergeben und ein neues EventHandlerArgument zurückgeben, das das Ursprungsereignisargument akzeptiert. Es ist MouseEventin diesem Beispiel.

Isolierte Funktionen halten jsx sauberer und verhindern, dass mehrere Flusenregeln verletzt werden. Wie jsx-no-bind, jsx-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);

Hier ist das Javascript-Snippet, wenn Sie mit Typoskript nicht vertraut sind:

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);
Jasperjian
quelle
2

Fügen Sie wie bei einer zustandslosen Komponente einfach eine Funktion hinzu -

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

und es heißt in der Rückgabe als onChange={addName}

Akarsh Srivastava
quelle
1

Wenn Sie nur wenige Funktionen in Ihren Requisiten haben, über die Sie sich Sorgen machen, können Sie dies tun:

let _dispatch = () => {};

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

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

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

Wenn es viel komplizierter wird, kehre ich normalerweise zu einer Klassenkomponente zurück.

jslatts
quelle
1

Nach ständiger Anstrengung hat es endlich für mich geklappt.

//..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>
);

Hinweis: Für Textfelder, die die Bindung "Name" -Attribut und "Eigenschaftsname" (z. B. model.name) enthalten, sollten nur "onInputTextChange" funktionieren. Die Logik "onInputTextChange" kann durch Ihren Code geändert werden.

Thulasiram
quelle
0

Wie wäre es mit so etwas:

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

Aber das ist wirklich übertrieben, wenn Sie den onClick nicht wie im Beispiel an niedrigere / innere Komponenten übergeben müssen.

Arnel Enero
quelle