Controladores de eventos en componentes sin estado de React

82

Tratando de encontrar una forma óptima de crear controladores de eventos en los componentes sin estado de React. Podría hacer algo como esto:

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

El inconveniente aquí es que cada vez que se procesa este componente, se crea una nueva función "myHandler". ¿Existe una mejor manera de crear controladores de eventos en componentes sin estado que aún pueden acceder a las propiedades del componente?

aStewartDesign
fuente
useCallback - const memoizedCallback = useCallback (() => {hacer Algo (a, b);}, [a, b],); Devuelve una devolución de llamada memorizada.
Shaik Md N Rasool

Respuestas:

61

La aplicación de controladores a elementos en componentes de funciones generalmente debería verse así:

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

Si necesita hacer algo mucho más complejo, es una señal de que a) el componente no debe ser sin estado (use una clase o ganchos), ob) debe crear el controlador en un componente contenedor externo con estado.

Como acotación al margen, y socavando ligeramente mi primer punto, a menos que el componente esté en una parte de la aplicación que se haya renderizado de manera particularmente intensa, no hay necesidad de preocuparse por crear funciones de flecha en render().

Jed Richards
fuente
2
¿Cómo evita esto crear una función cada vez que se procesa el componente sin estado?
zero_cool
1
El ejemplo de código anterior solo muestra un controlador que se aplica por referencia, no se crea una nueva función de controlador al renderizar ese componente. Si el componente externo creó el controlador usando useCallback(() => {}, [])o this.onClick = this.onClick.bind(this), entonces el componente también obtendría la misma referencia de controlador en cada renderizado, lo que podría ayudar con el uso de React.memoo shouldComponentUpdate(pero esto solo es relevante con componentes numerosos / complejos re-renderizados intensivamente).
Jed Richards
46

Usando la nueva función React hooks, podría verse así:

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

useCallback crea una función memorizada, lo que significa que una nueva función no se regenerará en cada ciclo de renderizado.

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

Sin embargo, esto aún se encuentra en la etapa de propuesta.

Ryan Vincent
fuente
7
React Hooks se han lanzado en React 16.8 y ahora son una parte oficial de React. Entonces esta respuesta funciona perfectamente.
cutemachine
3
Solo para tener en cuenta que la regla exhaustive-deps recomendada como parte del paquete eslint-plugin-react-hooks dice: "React Hook useCallback no hace nada cuando se llama con un solo argumento", así que, sí, en este caso, una matriz vacía debería pasado como segundo argumento.
olegzhermal
1
En su ejemplo anterior, no se gana eficiencia con el uso useCallback, y todavía está generando una nueva función de flecha en cada render (se pasa el argumento a useCallback). useCallbacksolo es útil cuando se pasan devoluciones de llamada a componentes secundarios optimizados que dependen de la igualdad de referencias para evitar representaciones innecesarias. Si solo está aplicando la devolución de llamada a un elemento HTML como un botón, no lo use useCallback.
Jed Richards
1
@JedRichards, aunque se crea una nueva función de flecha en cada renderizado, no es necesario actualizar el DOM, lo que debería ahorrar tiempo
herman
3
@herman No hay diferencia en absoluto (aparte de una pequeña penalización de rendimiento), por lo que esta respuesta que estamos comentando es un poco sospechosa :) Cualquier gancho que no tenga una matriz de dependencia se ejecutará después de cada actualización (se discute cerca del inicio de los documentos useEffect). Como mencioné, prácticamente solo querrá usar useCallback si desea una referencia estable / memorizada para una función de devolución de llamada que planea pasar a un componente secundario que se vuelve a renderizar de manera intensiva / costosa, y la igualdad referencial es importante. Cualquier otro uso, simplemente cree una nueva función en render cada vez.
Jed Richards
16

¿Qué tal de esta manera?

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

const myComponent = (props) => {
 return (
    <button onClick={(e) => myHandler(e,props)}>Click Me</button>
  );
}
Phi Nguyen
fuente
14
¡Buen pensamiento! Lamentablemente, esto no soluciona
aStewartDesign
@aStewartDesign alguna solución o actualización para este problema? Realmente me alegro de escucharlo, porque estoy enfrentando el mismo problema
Kim
4
tener un componente regular padre que tiene la implementación de myHandler y luego simplemente pasarlo al subcomponente
Raja Rao
Supongo que no hay mejor manera que esta hasta ahora (julio de 2018), si alguien encuentra algo interesante, por favor hágamelo saber
a_m_dev
¿por qué no <button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>? Quiero decir, no lo envuelva en una función myHandler.
Simon Franzen
6

Si el controlador se basa en propiedades que cambian, tendrá que crear el controlador cada vez, ya que carece de una instancia con estado en la que almacenarlo en caché. Otra alternativa que puede funcionar sería memorizar el controlador en función de los accesorios de entrada.

Opciones de implementación de pareja lodash._memoize R.memoize fast-memoize

Ryanjduffy
fuente
4

solución uno mapPropsToHandler y event.target.

Las funciones son objetos en js, por lo que es posible adjuntarles propiedades.

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

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

esta función solo vincula una vez una propiedad a una función.

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

Recibo mis accesorios así.

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

este ejemplo combinó tanto event.target como mapPropsToHandler. es mejor adjuntar funciones a los controladores solo, no números o cadenas. El número y las cadenas se pueden pasar con la ayuda del atributo DOM como

<select data-id={id}/>

en lugar de 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>
    )
}

solución dos. delegación de eventos

ver la solución uno. podemos eliminar el controlador de eventos de la entrada y ponerlo en su padre que también contiene otras entradas y, mediante la técnica de delegación de ayuda, podemos usar la función event.traget y mapPropsToHandler nuevamente.

Hassan Gilak
fuente
Mala práctica ! Una función solo debe cumplir su propósito, está destinada a ejecutar la lógica en algunos parámetros sin tener propiedades, solo porque javascript permite muchas formas creativas de hacer lo mismo no significa que deba permitirse usar lo que sea que funcione.
BeyondTheSea
4

Aquí está mi sencilla lista de productos favoritos implementada con react y redux escribiendo en mecanografiado. Puede pasar todos los argumentos que necesita en el controlador personalizado y devolver un EventHandlerargumento nuevo que acepte el evento de origen. Está MouseEventen este ejemplo.

Las funciones aisladas mantienen jsx más limpio y evitan que se rompan varias reglas de pelusa. Tales como 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);

Aquí está el fragmento de JavaScript si no está familiarizado con el mecanografiado:

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);
Jasperjian
fuente
2

Al igual que para un componente sin estado, simplemente agregue una función:

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

y se llama a la vuelta como onChange={addName}

Akarsh Srivastava
fuente
1

Si solo tiene algunas funciones en sus accesorios que le preocupan, puede hacer esto:

let _dispatch = () => {};

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

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

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

Si se vuelve mucho más complicado, normalmente vuelvo a tener un componente de clase.

jslatts
fuente
1

Después de un esfuerzo continuo finalmente funcionó para mí.

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

Nota: para el cuadro de texto archivado, el atributo "nombre" y el "nombre de la propiedad" (por ejemplo: model.name) deben ser iguales, entonces solo funcionará "onInputTextChange". La lógica "onInputTextChange" puede ser modificada por su código.

Thulasiram
fuente
0

Qué tal algo como esto:

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

pero realmente esto es excesivo si no necesita pasar el onClick a componentes inferiores / internos, como en el ejemplo.

Arnel Enero
fuente