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

90

我试图找出在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
10个回答

65

在函数组件中将处理程序应用于元素通常应该像这样:

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

如果您需要执行更复杂的操作,这表明组件要么不应该是无状态的(使用类或钩子),要么您应该在外部有状态的容器组件中创建处理程序。

另外,虽然这有点破坏了我的第一个观点,但除非组件在应用程序的特别频繁重新渲染的部分中,否则没有必要在 render() 中创建箭头函数。


2
这如何避免在每次呈现无状态组件时创建函数? - zero_cool
1
上面的代码示例只是展示了一个通过引用应用处理程序,没有在组件渲染时创建新的处理程序函数。如果外部组件使用 useCallback(() => {}, [])this.onClick = this.onClick.bind(this) 创建处理程序,则组件每次渲染都会获得相同的处理程序引用,这可以帮助使用 React.memoshouldComponentUpdate(但这仅适用于频繁重新渲染的大量/复杂组件)。 - Jed Richards

55

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

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
4
顺便提一下,eslint-plugin-react-hooks软件包中推荐的exhaustive-deps规则指出:"当仅使用一个参数调用React Hook useCallback时,它不会执行任何操作。"所以,在这种情况下,应该将一个空数组作为第二个参数传递。 - olegzhermal
2
在您上面的示例中,使用useCallback并没有提高效率 - 每次渲染仍会生成一个新的箭头函数(传递给useCallback的参数)。只有在将回调传递给依赖于引用相等性以防止不必要渲染的优化子组件时,useCallback才有用。如果您只是将回调应用于像按钮这样的HTML元素,则不要使用useCallback - Jed Richards
1
@JedRichards 尽管每次渲染都会创建一个新的箭头函数,但 DOM 不需要被更新,这应该可以节省时间。 - herman
4
除了一点性能上的损失外,它们没有任何区别,这就是为什么我们在评论下面进行的这个回答有些可疑 :) 任何没有依赖数组的钩子都将在每次更新后运行(它在 useEffect 文档的开头附近讨论)。就像我提到的那样,你几乎只需要在计划传递给一个经常重新渲染、耗费大量资源的子组件的回调函数的情况下,使用 useCallback 来获得稳定/记忆化的回调引用,并且引用相等性很重要。对于任何其他用法,只需在每次渲染时创建一个新函数即可。 - Jed Richards
显示剩余5条评论

17

这样怎么样:

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

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

16
好的,遗憾的是,这并不能解决每次呈现调用都创建一个新函数的问题:https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md - aStewartDesign
@aStewartDesign有关于这个问题的解决方案或更新吗?听到这个消息真是太好了,因为我也遇到了同样的问题。 - Kim
4
有一个父组件,其中包含myHandler的实现,然后只需将其传递给子组件即可。 - Raja Rao
我想目前(2018年7月)还没有比这更好的方法了,如果有人发现了什么酷的东西,请告诉我。 - amdev
为什么不这样写 <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>?我的意思是,不要将它包装在一个 myHandler 函数中。 - Simon Franzen
如果我将我的函数组件转换为类组件,应该可以解决这个问题。但我不确定使用类组件是否是适当的用例。 - kumarmo2

6
如果事件处理程序依赖于变化的属性,你需要每次创建处理程序,因为你缺少一个有状态的实例来缓存它。另一种可能可行的选择是基于输入 props 进行记忆化处理程序。
两种实现选项: lodash._memoize R.memoize fast-memoize

5
这是我用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代码片段:
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);

1
不,这个问题和你在每次渲染时重新创建函数的问题一样。你所做的只是让linter难以跟踪。 - JohnFlux

4

解决方案一:将mapPropsToHandler和event.target关联起来。

JavaScript中的函数是对象,因此可以为它们附加属性。

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.target和mapPropsToHandler函数。


2
不良实践!一个函数应该只服务于它的目的,它的作用是在一些参数上执行逻辑而不是持有属性,仅仅因为JavaScript允许很多创造性的方式来做同样的事情,并不意味着你应该让自己使用任何可行的方法。 - LucasBordeau

2

对于一个无状态组件,只需添加一个函数 -

最初的回答

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

并且在返回中被称为onChange={addName}

最初的回答:在返回中,它被称为onChange={addName}

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”的逻辑。


1
如果您只有几个在props中的函数让您感到担忧,您可以这样做:
let _dispatch = () => {};

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

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

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

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

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 传递给较低/内部组件,就像示例中一样,那么这真的是过度设计。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接