Обработчики событий в компонентах 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 N Rasool,

Ответы:


62

Применение обработчиков к элементам в функциональных компонентах обычно должно выглядеть так:

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

Если вам нужно сделать что-то гораздо более сложное, это признак того, что либо а) компонент не должен быть без состояния (используйте класс или хуки), либо б) вы должны создавать обработчик во внешнем компоненте-контейнере с сохранением состояния.

В стороне и немного подрывая мою первую мысль, если компонент не находится в особенно интенсивно повторно отрисовываемой части приложения, нет необходимости беспокоиться о создании стрелочных функций в render().


2
как это избежать создания функции каждый раз при рендеринге компонента без состояния?
zero_cool

1
В приведенном выше примере кода просто показан обработчик, применяемый по ссылке, при рендеринге этого компонента не создается новая функция обработчика. Если внешний компонент создал обработчик с помощью useCallback(() => {}, [])или this.onClick = this.onClick.bind(this), тогда компонент будет получать одну и ту же ссылку на обработчик при каждом рендеринге, что может помочь с использованием React.memoили shouldComponentUpdate(но это актуально только для интенсивно повторно отрисованных многочисленных / сложных компонентов).
Джед Ричардс,

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 Hooks были выпущены в React 16.8 и теперь являются официальной частью React. Итак, этот ответ работает отлично.
cutemachine

3
Просто обратите внимание, что рекомендуемое правило исчерпывающих зависимостей как часть пакета eslint-plugin-react-hooks гласит: «React Hook useCallback ничего не делает, когда вызывается только с одним аргументом», так что да, в этом случае должен быть пустой массив передается как второй аргумент.
олегжермал

1
В приведенном выше примере эффективность использования не повышается useCallback- и вы все еще генерируете новую стрелочную функцию при каждом рендеринге (переданный аргумент useCallback). useCallbackполезен только при передаче обратных вызовов оптимизированным дочерним компонентам, которые полагаются на равенство ссылок для предотвращения ненужных отрисовок. Если вы просто применяете обратный вызов к элементу HTML, например кнопке, не используйте useCallback.
Джед Ричардс

1
@JedRichards хотя новая функция стрелка создается на каждой визуализации, то DOM не нуждается в обновлении , которое должно сэкономить время
Herman

3
@herman Нет никакой разницы (кроме небольшого снижения производительности), поэтому этот ответ, который мы комментируем, является немного подозрительным :) Любой хук, у которого нет массива зависимостей, будет запускаться после каждого обновления (это обсуждается рядом с началом документации useEffect). Как я уже упоминал, вы в значительной степени захотите использовать useCallback только в том случае, если вам нужна стабильная / запомненная ссылка на функцию обратного вызова, которую вы планируете передать дочернему компоненту, который интенсивно / дорого перерисовывается, и ссылочное равенство важно. Любое другое использование, просто каждый раз создавайте новую функцию в рендере.
Джед Ричардс,

16

Как насчет этого:

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

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

15
Хорошая мысль! К сожалению, это не позволяет
решить

@aStewart Разработать какое-либо решение или обновление для этой проблемы? действительно рад это слышать, потому что у меня такая же проблема
Ким

4
иметь родительский обычный компонент, который имеет реализацию myHandler, а затем просто передать его подкомпоненту
Раджа Рао

я думаю, что нет лучшего способа, чем этот (июль 2018), если кто-то нашел что-то классное,
пожалуйста

почему бы и нет <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>? Я имею в виду, не оборачивайте его в функцию myHandler.
Саймон Францен

6

Если обработчик полагается на изменяющиеся свойства, вам придется каждый раз создавать обработчик, поскольку у вас нет экземпляра с отслеживанием состояния, на котором он мог бы кэшироваться. Другой альтернативой, которая может сработать, может быть мемоизация обработчика на основе входных свойств.

Пара вариантов реализации lodash._memoize R.memoize fast-memoize


4

решение one mapPropsToHandler и event.target.

функции являются объектами в js, поэтому их можно прикрепить к свойствам.

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

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

эта функция только один раз привязывает свойство к функции.

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

решение два. делегирование мероприятия

см. решение первое. мы можем удалить обработчик событий из ввода и передать его его родительскому элементу, который также содержит другие входные данные, а с помощью метода делегирования помощи мы можем снова использовать функцию event.traget и mapPropsToHandler.


Плохая практика! Функция должна служить только своей цели, она предназначена для выполнения логики для некоторых параметров, а не для хранения свойств, просто потому, что javascript допускает множество творческих способов сделать то же самое, не означает, что вы должны позволять себе использовать все, что работает.
BeyondTheSea

4

Вот мой простой список любимых продуктов, реализованный с использованием реакции и сокращения в машинописном тексте. Вы можете передать все необходимые аргументы в настраиваемый обработчик и вернуть новый, EventHandlerкоторый принимает аргумент исходного события. Это MouseEventв этом примере.

Изолированные функции сохраняют jsx чище и предотвращают нарушение нескольких правил линтинга. Такие , как 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);

Вот фрагмент кода 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» и «имя свойства» (например, 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 нижним / внутренним компонентам, как в примере.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.