Trình xử lý sự kiện trong các thành phần không trạng thái của React


82

Đang cố gắng tìm ra cách tối ưu để tạo trình xử lý sự kiện trong các thành phần không trạng thái của React. Tôi có thể làm điều gì đó như thế này:

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

Hạn chế ở đây là mỗi khi thành phần này được hiển thị, một hàm "myHandler" mới được tạo ra. Có cách nào tốt hơn để tạo trình xử lý sự kiện trong các thành phần không trạng thái vẫn có thể truy cập các thuộc tính của thành phần không?


useCallback - const memoizedCallback = useCallback (() => {doSomething (a, b);}, [a, b],); Trả về một cuộc gọi lại đã ghi nhớ.
Shaik Md N Rasool

Câu trả lời:


61

Việc áp dụng các trình xử lý cho các phần tử trong các thành phần hàm thường trông giống như sau:

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

Nếu bạn cần làm bất cứ điều gì phức tạp hơn nhiều, đó là dấu hiệu cho thấy a) thành phần không được ở trạng thái không trạng thái (sử dụng một lớp hoặc hook) hoặc b) bạn nên tạo trình xử lý trong một thành phần chứa trạng thái bên ngoài.

Bỏ qua một bên và làm suy yếu điểm đầu tiên của tôi một chút, trừ khi thành phần nằm trong một phần đặc biệt được kết xuất lại một cách chuyên sâu của ứng dụng thì không cần phải lo lắng về việc tạo các hàm mũi tên trong đó render().


2
Làm thế nào để điều này tránh tạo một hàm mỗi khi thành phần không trạng thái được hiển thị?
zero_cool

1
Ví dụ mã ở trên chỉ hiển thị một trình xử lý đang được áp dụng bằng tham chiếu, không có chức năng xử lý mới nào được tạo khi kết xuất thành phần đó. Nếu thành phần bên ngoài đã tạo trình xử lý bằng cách sử dụng useCallback(() => {}, [])hoặc this.onClick = this.onClick.bind(this), thì thành phần đó cũng sẽ nhận được cùng một tham chiếu trình xử lý mà mỗi lần hiển thị cũng có thể giúp sử dụng React.memohoặc shouldComponentUpdate(nhưng điều này chỉ liên quan với nhiều thành phần phức tạp được kết xuất lại một cách chuyên sâu).
Jed Richards

46

Sử dụng tính năng React hooks mới, nó có thể trông giống như sau:

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

useCallback tạo một hàm ghi nhớ, nghĩa là một hàm mới sẽ không được tạo lại trên mỗi chu kỳ kết xuất.

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

Tuy nhiên, điều này vẫn đang ở giai đoạn đề xuất.


7
React Hooks đã được phát hành trong React 16.8 và hiện là một phần chính thức của React. Vì vậy, câu trả lời này hoạt động hoàn hảo.
cutemachine

3
Chỉ cần lưu ý rằng quy tắc expustive-deps được đề xuất như một phần của gói eslint-plugin-react-hooks cho biết: "React Hook useCallback không làm gì khi được gọi chỉ với một đối số.", Vì vậy, vâng, trong trường hợp này là một mảng trống được truyền như một đối số thứ hai.
olegzhermal

1
Trong ví dụ của bạn ở trên, không có hiệu quả đạt được khi sử dụng useCallback- và bạn vẫn đang tạo một hàm mũi tên mới mỗi lần hiển thị (đối số được chuyển tới useCallback). useCallbackchỉ hữu ích khi chuyển lệnh gọi lại đến các thành phần con được tối ưu hóa dựa trên bình đẳng tham chiếu để ngăn các kết xuất không cần thiết. Nếu bạn chỉ áp dụng lệnh gọi lại cho một nút like phần tử HTML thì không sử dụng useCallback.
Jed Richards

1
@JedRichards mặc dù một mũi tên chức năng mới được tạo ra trên mỗi render, DOM không cần phải được cập nhật mà nên tiết kiệm thời gian
herman

3
@herman Không có sự khác biệt nào cả (ngoài một hình phạt hiệu suất nhỏ), đó là lý do tại sao câu trả lời mà chúng tôi đang bình luận này hơi đáng ngờ :) Bất kỳ hook nào không có mảng phụ thuộc sẽ chạy sau mỗi lần cập nhật (nó được thảo luận gần khi bắt đầu sử dụng Hiệu ứng tài liệu). Giống như tôi đã đề cập, bạn hầu như chỉ muốn sử dụng useCallback nếu bạn muốn có một ref ổn định / được ghi nhớ cho một hàm gọi lại mà bạn đang dự định chuyển tới một thành phần con được kết xuất lại một cách sâu sắc / tốn kém và bình đẳng tham chiếu là quan trọng. Bất kỳ cách sử dụng nào khác, chỉ cần tạo một chức năng mới trong kết xuất mỗi lần.
Jed Richards

16

Làm thế nào về cách này:

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

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

14
Tư tưởng tốt! Đáng buồn thay, điều này không có được xung quanh vấn đề của việc tạo ra một chức năng mới mỗi làm cho cuộc gọi: github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/...
aStewartDesign

@aStewartDesign bất kỳ giải pháp hoặc bản cập nhật nào cho vấn đề này? thực sự vui mừng khi biết điều đó, bởi vì tôi đang đối mặt với cùng một vấn đề
Kim

4
có một thành phần thường cha mẹ có việc thực hiện myHandler và sau đó chỉ cần vượt qua nó để tiểu hợp phần
Raja Rao

tôi đoán không có cách nào tốt hơn mà cái này cho đến nay (tháng 7 năm 2018), nếu bất kỳ một tìm thấy một số điều thú vị, xin cho tôi biết
a_m_dev

tại sao không <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>? Ý tôi là, không bọc nó trong một func myHandler.
Simon Franzen

6

Nếu trình xử lý dựa vào các thuộc tính thay đổi, bạn sẽ phải tạo trình xử lý mỗi lần vì bạn thiếu thể hiện trạng thái để lưu vào bộ nhớ cache. Một giải pháp thay thế khác có thể hoạt động là ghi nhớ trình xử lý dựa trên các đạo cụ đầu vào.

Các tùy chọn triển khai cặp đôi lodash._memoize R.memoize fast-memoize


4

giải pháp một mapPropsToHandler và event.target.

các hàm là các đối tượng trong js nên có thể đính kèm các thuộc tính của chúng.

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

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

chức năng này chỉ liên kết một thuộc tính với một chức năng.

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

Tôi nhận được đạo cụ của mình giống như thế này.

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

ví dụ này kết hợp cả event.target và mapPropsToHandler. tốt hơn hết là chỉ đính kèm các hàm vào trình xử lý chứ không phải số hoặc chuỗi. số và chuỗi có thể được chuyển với sự trợ giúp của thuộc tính DOM như

<select data-id={id}/>

chứ không phải 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>
    )
}

giải pháp hai. đoàn sự kiện

xem giải pháp một. chúng ta có thể xóa trình xử lý sự kiện khỏi đầu vào và đặt nó cho cha mẹ của nó cũng chứa các đầu vào khác và bằng kỹ thuật ủy quyền trợ giúp, chúng ta có thể sử dụng lại hàm event.traget và mapPropsToHandler.


Thực hành tệ! Một hàm chỉ nên phục vụ mục đích của nó, nó có nghĩa là thực thi logic trên một số tham số không phải để giữ các thuộc tính, chỉ vì javascript cho phép nhiều cách sáng tạo để làm điều tương tự không có nghĩa là bạn nên để mình sử dụng bất cứ thứ gì hiệu quả.
BeyondTheSea

4

Đây là danh sách các sản phẩm yêu thích đơn giản của tôi được triển khai bằng cách viết react và redux trong typecript. Bạn có thể chuyển tất cả các đối số bạn cần trong trình xử lý tùy chỉnh và trả về một EventHandlerđối số mới chấp nhận đối số sự kiện gốc. Nó MouseEventnằm trong ví dụ này.

Các hàm biệt lập giữ cho jsx sạch hơn và tránh vi phạm một số quy tắc linting. Chẳng hạn như 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);

Đây là đoạn mã javascript nếu bạn không quen thuộc với 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

Giống như đối với một thành phần không trạng thái, chỉ cần thêm một hàm -

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

và nó được gọi là onChange={addName}


1

Nếu bạn chỉ có một vài chức năng trong đạo cụ mà bạn lo lắng, bạn có thể làm điều này:

let _dispatch = () => {};

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

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

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

Nếu nó trở nên phức tạp hơn nhiều, tôi thường chỉ quay lại việc có một thành phần lớp.


1

Sau nỗ lực liên tục cuối cùng cũng có hiệu quả với tôi.

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

Lưu ý: đối với hộp văn bản có ràng buộc thuộc tính "name" và "tên thuộc tính" (ví dụ: model.name) phải giống nhau thì chỉ "onInputTextChange" mới hoạt động. Mã của bạn có thể sửa đổi logic "onInputTextChange".


0

Còn những thứ như thế này thì sao:

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

nhưng thực sự điều này là quá mức cần thiết nếu bạn không cần chuyển onClick đến các thành phần bên trong / thấp hơn, như trong ví dụ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.