Programy obsługi zdarzeń w komponentach bezstanowych React

84

Próbuję znaleźć optymalny sposób tworzenia programów obsługi zdarzeń w komponentach bezstanowych React. Mógłbym zrobić coś takiego:

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

Wadą jest to, że za każdym razem, gdy ten komponent jest renderowany, tworzona jest nowa funkcja „myHandler”. Czy istnieje lepszy sposób tworzenia programów obsługi zdarzeń w składnikach bezstanowych, które nadal mają dostęp do właściwości składnika?

aStewartDesign
źródło
useCallback - const memoizedCallback = useCallback (() => {doSomething (a, b);}, [a, b],); Zwraca zapamiętane oddzwonienie.
Shaik Md N Rasool

Odpowiedzi:

62

Stosowanie programów obsługi do elementów w komponentach funkcji powinno generalnie wyglądać tak:

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

Jeśli potrzebujesz zrobić coś bardziej złożonego, jest to znak, że albo a) składnik nie powinien być bezstanowy (użyj klasy lub haków), albo b) powinieneś utworzyć procedurę obsługi w zewnętrznym składniku kontenera stanowego.

Na marginesie i nieznacznie podważając mój pierwszy punkt, chyba że komponent znajduje się w szczególnie intensywnie ponownie renderowanej części aplikacji, nie ma potrzeby martwić się o tworzenie funkcji strzałek w render().

Jed Richards
źródło
2
w jaki sposób można uniknąć tworzenia funkcji za każdym razem, gdy renderowany jest składnik bezstanowy?
zero_cool
1
Powyższy przykład kodu pokazuje tylko procedurę obsługi stosowaną przez odniesienie, żadna nowa funkcja obsługi nie jest tworzona podczas renderowania tego komponentu. Jeśli komponent zewnętrzny utworzył procedurę obsługi przy użyciu useCallback(() => {}, [])lub this.onClick = this.onClick.bind(this), wówczas komponent otrzymywałby to samo odwołanie do procedury obsługi przy każdym renderowaniu, co mogłoby pomóc w użyciu React.memolub shouldComponentUpdate(ale ma to znaczenie tylko w przypadku intensywnie ponownie renderowanych / złożonych komponentów).
Jed Richards,
48

Korzystając z nowej funkcji hooków React, może to wyglądać mniej więcej tak:

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

useCallback tworzy funkcję zapamiętaną, co oznacza, że ​​nowa funkcja nie będzie ponownie generowana w każdym cyklu renderowania.

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

Jednak jest to nadal na etapie wniosku.

ryanVincent
źródło
7
React Hooks zostały wydane w React 16.8 i są teraz oficjalną częścią React. Więc ta odpowiedź działa doskonale.
cutemachine
3
Wystarczy zauważyć, że zalecana reguła wyczerpującego deps jako część pakietu eslint-plugin-react-hooks mówi: "React Hook useCallback nic nie robi, gdy jest wywoływany z tylko jednym argumentem.", Więc tak, w tym przypadku pusta tablica powinna być przekazany jako drugi argument.
olegzhermal
1
W powyższym przykładzie nie ma żadnej wydajności uzyskanej przy użyciu useCallback- i nadal generujesz nową funkcję strzałkową przy każdym renderowaniu (argument przekazany do useCallback). useCallbackjest użyteczne tylko podczas przekazywania wywołań zwrotnych do zoptymalizowanych komponentów potomnych, które polegają na równości odwołań, aby zapobiec niepotrzebnym renderowaniu. Jeśli tylko stosujesz wywołanie zwrotne do elementu HTML, takiego jak przycisk, nie używaj useCallback.
Jed Richards
1
@JedRichards chociaż nowa funkcja strzałki jest tworzona przy każdym renderowaniu, DOM nie musi być aktualizowany, co powinno zaoszczędzić czas
herman
3
@herman Nie ma żadnej różnicy (poza niewielkim spadkiem wydajności), dlatego ta odpowiedź, pod którą komentujemy, jest nieco podejrzana :) Każdy punkt zaczepienia, który nie ma tablicy zależności, będzie uruchamiany po każdej aktualizacji (jest to omówione blisko początku dokumentacji useEffect). Jak wspomniałem, chciałbyś używać useCallback tylko wtedy, gdy potrzebujesz stabilnego / zapamiętanego odwołania do funkcji zwrotnej, którą planujesz przekazać do komponentu potomnego, który jest intensywnie / kosztownie renderowany ponownie, a równość referencyjna jest ważna. Każde inne użycie, po prostu utwórz za każdym razem nową funkcję w renderowaniu.
Jed Richards
16

A może w ten sposób:

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

const myComponent = (props) => {
 return (
    <button onClick={(e) => myHandler(e,props)}>Click Me</button>
  );
}
Phi Nguyen
źródło
15
Dobra myśl! Niestety, nie pozwala to obejść problemu tworzenia nowej funkcji przy każdym wywołaniu renderowania: github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/…
aStewartDesign
@aStewartDesign jakieś rozwiązanie lub aktualizacja dla tego problemu? bardzo się cieszę, że to słyszę, ponieważ mam ten sam problem
Kim
4
mieć macierzysty zwykły komponent, który ma implementację myHandler, a następnie po prostu przekaż go do podkomponentu
Raja Rao
chyba nie ma lepszego sposobu niż ten do tej pory (lipiec 2018), jeśli ktoś znalazł coś fajnego, daj mi znać
a_m_dev
dlaczego nie <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>? To znaczy, nie zawijaj tego w funkcję myHandler.
Simon Franzen
6

Jeśli procedura obsługi opiera się na właściwościach, które się zmieniają, za każdym razem będziesz musiał utworzyć procedurę obsługi, ponieważ brakuje stanowej instancji, w której można ją buforować. Inną alternatywą, która może zadziałać, byłoby zapamiętanie programu obsługi na podstawie właściwości wejściowych.

Kilka opcji implementacji lodash._memoize R.memoize fast-memoize

ryanjduffy
źródło
4

rozwiązanie jeden mapPropsToHandler i event.target.

funkcje są obiektami w js, więc można do nich dołączyć właściwości.

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

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

ta funkcja wiąże właściwość tylko raz z funkcją.

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

Dostaję swoje rekwizyty właśnie w ten sposób.

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

w tym przykładzie połączono zarówno event.target, jak i mapPropsToHandler. lepiej jest dołączać funkcje tylko do programów obsługi, a nie do liczb lub łańcuchów. liczba i ciągi mogą być przekazywane za pomocą atrybutu DOM, takiego jak

<select data-id={id}/>

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

rozwiązanie drugie. delegowanie wydarzeń

zobacz rozwiązanie pierwsze. możemy usunąć program obsługi zdarzeń z wejścia i umieścić go w jego rodzicu, który również przechowuje inne dane wejściowe, a dzięki technice delegowania pomocy możemy ponownie użyć funkcji event.traget i mapPropsToHandler.

Hassan Gilak
źródło
Zła praktyka! Funkcja powinna służyć tylko swojemu celowi, ma na celu wykonywanie logiki na niektórych parametrach, aby nie przechowywać właściwości, tylko dlatego, że javascript pozwala na wiele kreatywnych sposobów zrobienia tego samego, nie oznacza, że ​​powinieneś pozwolić sobie na użycie tego, co działa.
BeyondTheSea
4

Oto moja prosta lista ulubionych produktów zaimplementowana z pisaniem na maszynie React i Redux. Możesz przekazać wszystkie potrzebne argumenty w niestandardowej obsłudze i zwrócić nową, EventHandlerktóra akceptuje argument zdarzenia pochodzenia. To jest MouseEventw tym przykładzie.

Wyizolowane funkcje utrzymują jsx w czystości i zapobiegają łamaniu kilku zasad lintingu. Takie jak 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);

Oto fragment kodu javascript, jeśli nie znasz maszynopisu:

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
źródło
2

Podobnie jak w przypadku składnika bezstanowego, wystarczy dodać funkcję -

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

aw zamian nazywany jest jako onChange={addName}

Akarsh Srivastava
źródło
1

Jeśli masz tylko kilka funkcji w swoich rekwizytach, o które się martwisz, możesz to zrobić:

let _dispatch = () => {};

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

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

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

Jeśli robi się to znacznie bardziej skomplikowane, zwykle wracam do komponentu klasowego.

jslatts
źródło
1

Po ciągłym wysiłku w końcu u mnie zadziałało.

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

Uwaga: dla powiązania pola tekstowego atrybut "nazwa" i "nazwa właściwości" (np. Model.name) powinny być takie same, wtedy będzie działać tylko "onInputTextChange". Logikę „onInputTextChange” można modyfikować za pomocą kodu.

Thulasiram
źródło
0

Co powiesz na coś takiego:

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

ale naprawdę jest to przesada, jeśli nie musisz przekazywać onClick do niższych / wewnętrznych komponentów, jak w przykładzie.

Arnel Enero
źródło