Обробники подій у компонентах 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 були випущені в React 16.8 і тепер є офіційною частиною React. Тож ця відповідь працює чудово.
cutemachine

3
Тільки зауважимо, що рекомендоване правило вичерпних описів як частина пакета eslint-plugin-response-hooks говорить: "React Hook useCallback нічого не робить, коли викликається лише одним аргументом.", Отже, так, у цьому випадку повинен бути порожній масив передається як другий аргумент.
olegzhermal

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

1
@JedRichards, хоча на кожному рендері створюється нова функція стрілки, DOM не потрібно оновлювати, що має заощадити час
Герман

3
@herman Різниці взагалі немає (окрім невеликого покарання за продуктивність), саме тому ця відповідь, яку ми коментуємо, є трохи підозрілою :) Будь-який хук, який не має масиву залежностей, запускатиметься після кожного оновлення (це обговорюється на початку використання useEffect docs). Як я вже згадував, ви б майже хотіли використовувати useCallback лише тоді, коли вам потрібна стабільна / запам'ятовувана посилання на функцію зворотного виклику, яку ви плануєте передати дочірньому компоненту, який інтенсивно / дорого відтворюється, а посилальна рівність важлива. Будь-яке інше використання, просто створюйте нову функцію в рендері щоразу.
Джед Річардс,

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/eslint-plugin-react/blob/master/docs/rules/…
aStewartDesign

@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

Ось мій простий список улюблених продуктів, реалізований із написанням React та Redux на машинописі. Ви можете передати всі необхідні аргументи у спеціальному обробнику та повернути новий, 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, якщо ви не знайомі з typecript:

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" і "name 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.