React 无状态组件中的事件处理程序

Posted

技术标签:

【中文标题】React 无状态组件中的事件处理程序【英文标题】:Event Handlers in React Stateless Components 【发布时间】:2017-01-08 16:37:22 【问题描述】:

试图找出在 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], );返回一个记忆回调。 【参考方案1】:

就像无状态组件一样,只需添加一个函数 -

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

它在返回中被称为onChange=addName

【讨论】:

【参考方案2】:

将处理程序应用于函数组件中的元素通常应该如下所示:

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

如果您需要做任何更复杂的事情,这表明 a) 组件不应该是无状态的(使用类或钩子),或者 b) 您应该在外部有状态容器组件中创建处理程序.

顺便说一句,稍微削弱我的第一点,除非该组件在应用程序的一个特别密集的重新渲染部分中,否则无需担心在render() 中创建箭头函数。

【讨论】:

如何避免每次渲染无状态组件时创建一个函数? 上面的代码示例只是显示了一个通过引用应用的处理程序,在渲染该组件时没有创建新的处理程序函数。如果外部组件使用useCallback(() =&gt; , [])this.onClick = this.onClick.bind(this) 创建了处理程序,那么该组件将在每次渲染时获得相同的处理程序引用,这有助于使用React.memoshouldComponentUpdate(但这仅与密集重新-渲染了许多/复杂的组件)。【参考方案3】:

这样的事情怎么样:

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 传递给较低/内部组件,那么这确实是大材小用。

【讨论】:

【参考方案4】:

使用新的 React 钩子功能,它可能看起来像这样:

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

useCallback 创建一个记忆函数,这意味着不会在每个渲染周期重新生成一个新函数。

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

但是,这仍处于提案阶段。

【讨论】:

React Hooks 已在 React 16.8 中发布,现在是 React 的官方部分。所以这个答案很完美。 请注意,作为 eslint-plugin-react-hooks 包的一部分,推荐的穷举-deps 规则说:“当仅使用一个参数调用时,React Hook useCallback 什么都不做。”,所以,是的,在这种情况下,应该将一个空数组作为第二个参数传递。 在您上面的示例中,使用useCallback 并没有提高效率 - 而且您仍然在每次渲染时生成一个新的箭头函数(传递给useCallback 的arg)。 useCallback 仅在将回调传递给优化的子组件时有用,这些子组件依赖于引用相等来防止不必要的渲染。如果您只是将回调应用于 HTML 元素(如按钮),则不要使用 useCallback @JedRichards 虽然每次渲染都会创建一个新的箭头函数,但不需要更新 DOM,这样可以节省时间 @herman 完全没有区别(除了小的性能损失),这就是为什么我们评论的这个答案有点可疑:) 任何没有依赖数组的钩子将在每次更新后运行(在 useEffect 文档的开头附近进行了讨论)。就像我提到的那样,如果您想要一个稳定/记忆的回调函数引用,您几乎只想使用 useCallback,您计划将其传递给密集/昂贵的重新渲染的子组件,并且引用相等很重要。其他任何用法,每次在render中新建一个函数即可。【参考方案5】:

经过不断的努力终于为我奏效了。

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

注意:对于文本框字段绑定“名称”属性和“属性名称”(例如:model.name)应该相同,然后只有“onInputTextChange”可以工作。 “onInputTextChange”逻辑可以通过你的代码进行修改。

【讨论】:

【参考方案6】:

这是我最喜欢的简单产品列表,用 typescript 编写的 react 和 redux 实现。您可以在自定义处理程序中传递您需要的所有参数并返回一个新的EventHandler,它接受原始事件参数。在这个例子中是MouseEvent

隔离函数使 jsx 更干净,并防止破坏多个 linting 规则。如jsx-no-bindjsx-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);

如果你不熟悉 typescript,这里是 javascript sn-p:

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

【讨论】:

不,这与您在每次渲染时重新创建函数的问题相同。你所做的只是让 linter 跟踪起来过于复杂。【参考方案7】:

解决方案一 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 允许许多创造性的方式来做同样的事情并不意味着你应该让自己使用任何有效的方法。【参考方案8】:

如果你的道具中只有几个你担心的功能,你可以这样做:

let _dispatch = () => ;

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

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

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

如果它变得更复杂,我通常会回到使用类组件。

【讨论】:

【参考方案9】:

如果处理程序依赖于发生变化的属性,则您必须每次都创建处理程序,因为您缺少缓存它的有状态实例。另一种可行的方法是根据输入道具来记忆处理程序。

几个实现选项 lodash._memoize R.memoize fast-memoize

【讨论】:

【参考方案10】:

这样怎么样:

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

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

【讨论】:

好主意!遗憾的是,这并没有解决每次渲染调用都创建一个新函数的问题:github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/… @aStewartDesign 对此问题有任何解决方案或更新吗?真的很高兴听到它,因为我面临同样的问题 有一个父常规组件,该组件具有 myHandler 的实现,然后简单地将其传递给子组件 我想到目前为止(2018 年 7 月)没有更好的方法,如果有人发现一些很酷的东西,请告诉我 为什么不&lt;button onClick=(e) =&gt; props.dispatch(e,props.whatever)&gt;Click Me&lt;/button&gt;?我的意思是,不要将它包装在 myHandler 函数中。

以上是关于React 无状态组件中的事件处理程序的主要内容,如果未能解决你的问题,请参考以下文章

React4.事件处理与类组件状态

React 复选框事件和处理程序的打字稿类型?

React组件基础

React组件基础

如何在纯函数式 React 组件中处理事件

从 React 中的子组件调用父组件中定义的事件处理程序