javascript – Event Handlers in React Stateless Components-ThrowExceptions

Exception or error:

Trying to figure out an optimal way to create event handlers in React stateless components. I could do something like this:

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

The drawback here being that every time this component is rendered, a new “myHandler” function is created. Is there a better way to create event handlers in stateless components that can still access the component properties?

How to solve:

Applying handlers to elements in function components should generally just look like this:

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

If you need to do anything much more complex it’s a sign that either a) the component shouldn’t be stateless (use a class, or hooks), or b) you should be creating the handler in an outer stateful container component.

As an aside, and undermining my first point slightly, unless the component is in a particularly intensively re-rendered part of the app there’s no need to worry about creating arrow functions in render().

###

Using the new React hooks feature it could look something like this:

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

useCallback creates a memoised function, meaning a new function will not be regenerated on each render cycle.

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

However, this is still at proposal stage.

###

How about this way :

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

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

###

If the handler relies on properties that change, you will have to create the handler each time since you lack a stateful instance on which to cache it. Another alternative which may work would be to memoize the handler based on the input props.

Couple implementation options
lodash._memoize
R.memoize
fast-memoize

###

solution one mapPropsToHandler and event.target.

functions are objects in js so its possible to attach them properties.

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

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

this function only bind once a property to a function.

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

I do get my props just like this.

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

this example combined both event.target and mapPropsToHandler. its better to attach functions to handlers only not numbers or strings. number and strings could be passed with help of DOM attribute like

<select data-id={id}/>

rather than 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 two. event delegation

see solution one. we can remove event handler from input and put it to its parent that holds other inputs too and by the help delegation technique we can be use event.traget and mapPropsToHandler function again.

###

Here is my simple favorite products list implemented with react and redux writing in typescript. You can pass all arguments you need in the custom handler and return a new EventHandler which accepts origin event argument. It’s MouseEvent in this example.

Isolated functions keep jsx cleaner and prevent from breaking several linting rules. Such as 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);

Here is the javascript snippet if you are not familiar with 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);

###

If you only have a few functions in you props that you are worried about you can do this:

let _dispatch = () => {};

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

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

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

If it gets much more complicated, I usually just go back to having a class component.

###

After continuous effort finally worked for me.

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

Note: for text box filed binding “name” attribute & “property name” (e.g: model.name) should be same then only “onInputTextChange” will work.
“onInputTextChange” logic can be modified by your code.

###

Like for a stateless component, just add a function –

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

and it is called in the return as onChange={addName}

###

How about something like this:

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

but really this is overkill if you don’t need to pass the onClick to lower/inner components, like in the example.

Leave a Reply

Your email address will not be published. Required fields are marked *