web-dev-qa-db-fra.com

Gestionnaires d'événements dans les composants React sans état

Essayer de trouver un moyen optimal de créer des gestionnaires d’événements dans les composants React sans état. Je pourrais faire quelque chose comme ça:

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

L'inconvénient est que chaque fois que ce composant est rendu, une nouvelle fonction "myHandler" est créée. Existe-t-il un meilleur moyen de créer des gestionnaires d'événements dans des composants sans état pouvant toujours accéder aux propriétés du composant?

32
aStewartDesign

L'application de gestionnaires aux éléments dans les composants de fonction doit généralement ressembler à ceci:

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

Si vous devez faire quelque chose de plus complexe, comme créer des gestionnaires, les lier ou quoi que ce soit, cela signifie: a) que le composant ne doit pas être sans état ou b) vous devez créer le gestionnaire dans un conteneur externe avec état composant.

En passant, et sapant légèrement mon premier point, à moins que le composant ne soit dans une partie particulièrement intensément restituée de l'application, il n'est pas nécessaire de s'inquiéter de la création de fonctions fléchées dans render().

36
Jed Richards

Que diriez-vous de cette façon:

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

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

En utilisant la nouvelle fonctionnalité de réactifs React, cela pourrait ressembler à ceci:

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

useCallback crée une fonction mémo, ce qui signifie qu'une nouvelle fonction ne sera pas régénérée à chaque cycle de rendu.

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

Cependant, nous en sommes encore au stade de la proposition.

5
ryanVincent

Si le gestionnaire s'appuie sur des propriétés qui changent, vous devrez le créer à chaque fois car il vous manque une instance avec état sur laquelle le mettre en cache. Une autre alternative qui pourrait fonctionner serait de mémoriser le gestionnaire en fonction des accessoires d’entrée.

Coupler les options de mise en œuvre lodash._memoizeR.memoizefast-memoize

4
ryanjduffy

solution one mapPropsToHandler et event.target.

les fonctions sont des objets dans js, il est donc possible de les associer à des propriétés.

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

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

cette fonction ne lie qu'une fois une propriété à une fonction.

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

Je reçois mes accessoires comme ça.

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

cet exemple combine event.target et mapPropsToHandler. il est préférable d’attacher des fonctions aux gestionnaires mais pas aux nombres ou aux chaînes. nombre et les chaînes pourraient être passés à l'aide d'attribut DOM comme 

<select data-id={id}/>

plutôt que 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>
    )
}

solution deux. délégation d'événement

voir la première solution. nous pouvons supprimer le gestionnaire d'événements de l'entrée et le placer dans son parent qui contient également d'autres entrées. Par la technique de délégation de l'aide, nous pouvons à nouveau utiliser les fonctions event.traget et mapPropsToHandler. 

2
Hassan Gilak

Voici ma liste de produits préférés simple implémentée avec react et redux écrit en TypeScript. Vous pouvez transmettre tous les arguments nécessaires dans le gestionnaire personnalisé et renvoyer une nouvelle variable EventHandler qui accepte l'argument de l'événement Origin. C'est MouseEvent dans cet exemple.

Des fonctions isolées gardent jsx plus propre et empêchent de casser plusieurs règles de peluchage. Tels que 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);

Voici l'extrait de code javascript si vous n'êtes pas familier avec TypeScript:

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
jasperjian

Si vous n'avez que quelques fonctions dans vos accessoires qui vous inquiètent, vous pouvez le faire:

let _dispatch = () => {};

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

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

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

Si cela devient beaucoup plus compliqué, je reviens généralement à un cours.

0
jslatts

Que diriez-vous quelque chose comme ça:

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

mais c'est vraiment exagéré si vous n'avez pas besoin de passer le onClick aux composants inférieurs/internes, comme dans l'exemple.

0
Arnel Enero

Après des efforts continus, j'ai finalement travaillé.

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

Remarque: pour l'attribut "nom" et le "nom de propriété" (par exemple: model.name), la liaison de zone de texte doit être identique, alors seul "onInputTextChange" fonctionnera. La logique "onInputTextChange" peut être modifiée par votre code.

0
Thulasiram