Próbuję znaleźć optymalny sposób tworzenia programów obsługi zdarzeń w komponentach bezstanowych React. Mógłbym zrobić coś takiego:
const myComponent = (props) => {
const myHandler = (e) => props.dispatch(something());
return (
<button onClick={myHandler}>Click Me</button>
);
}
Wadą jest to, że za każdym razem, gdy ten komponent jest renderowany, tworzona jest nowa funkcja „myHandler”. Czy istnieje lepszy sposób tworzenia programów obsługi zdarzeń w składnikach bezstanowych, które nadal mają dostęp do właściwości składnika?
javascript
reactjs
aStewartDesign
źródło
źródło
Odpowiedzi:
Stosowanie programów obsługi do elementów w komponentach funkcji powinno generalnie wyglądać tak:
const f = props => <button onClick={props.onClick}></button>
Jeśli potrzebujesz zrobić coś bardziej złożonego, jest to znak, że albo a) składnik nie powinien być bezstanowy (użyj klasy lub haków), albo b) powinieneś utworzyć procedurę obsługi w zewnętrznym składniku kontenera stanowego.
Na marginesie i nieznacznie podważając mój pierwszy punkt, chyba że komponent znajduje się w szczególnie intensywnie ponownie renderowanej części aplikacji, nie ma potrzeby martwić się o tworzenie funkcji strzałek w
render()
.źródło
useCallback(() => {}, [])
lubthis.onClick = this.onClick.bind(this)
, wówczas komponent otrzymywałby to samo odwołanie do procedury obsługi przy każdym renderowaniu, co mogłoby pomóc w użyciuReact.memo
lubshouldComponentUpdate
(ale ma to znaczenie tylko w przypadku intensywnie ponownie renderowanych / złożonych komponentów).Korzystając z nowej funkcji hooków React, może to wyglądać mniej więcej tak:
const HelloWorld = ({ dispatch }) => { const handleClick = useCallback(() => { dispatch(something()) }) return <button onClick={handleClick} /> }
useCallback
tworzy funkcję zapamiętaną, co oznacza, że nowa funkcja nie będzie ponownie generowana w każdym cyklu renderowania.https://reactjs.org/docs/hooks-reference.html#usecallback
Jednak jest to nadal na etapie wniosku.
źródło
useCallback
- i nadal generujesz nową funkcję strzałkową przy każdym renderowaniu (argument przekazany douseCallback
).useCallback
jest użyteczne tylko podczas przekazywania wywołań zwrotnych do zoptymalizowanych komponentów potomnych, które polegają na równości odwołań, aby zapobiec niepotrzebnym renderowaniu. Jeśli tylko stosujesz wywołanie zwrotne do elementu HTML, takiego jak przycisk, nie używajuseCallback
.A może w ten sposób:
const myHandler = (e,props) => props.dispatch(something()); const myComponent = (props) => { return ( <button onClick={(e) => myHandler(e,props)}>Click Me</button> ); }
źródło
<button onClick={(e) => props.dispatch(e,props.whatever)}>Click Me</button>
? To znaczy, nie zawijaj tego w funkcję myHandler.Jeśli procedura obsługi opiera się na właściwościach, które się zmieniają, za każdym razem będziesz musiał utworzyć procedurę obsługi, ponieważ brakuje stanowej instancji, w której można ją buforować. Inną alternatywą, która może zadziałać, byłoby zapamiętanie programu obsługi na podstawie właściwości wejściowych.
Kilka opcji implementacji lodash._memoize R.memoize fast-memoize
źródło
rozwiązanie jeden mapPropsToHandler i event.target.
funkcje są obiektami w js, więc można do nich dołączyć właściwości.
function onChange() { console.log(onChange.list) } function Input(props) { onChange.list = props.list; return <input onChange={onChange}/> }
ta funkcja wiąże właściwość tylko raz z funkcją.
export function mapPropsToHandler(handler, props) { for (let property in props) { if (props.hasOwnProperty(property)) { if(!handler.hasOwnProperty(property)) { handler[property] = props[property]; } } } }
Dostaję swoje rekwizyty właśnie w ten sposób.
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) }
w tym przykładzie połączono zarówno event.target, jak i mapPropsToHandler. lepiej jest dołączać funkcje tylko do programów obsługi, a nie do liczb lub łańcuchów. liczba i ciągi mogą być przekazywane za pomocą atrybutu DOM, takiego jak
zamiast 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> ) }
rozwiązanie drugie. delegowanie wydarzeń
zobacz rozwiązanie pierwsze. możemy usunąć program obsługi zdarzeń z wejścia i umieścić go w jego rodzicu, który również przechowuje inne dane wejściowe, a dzięki technice delegowania pomocy możemy ponownie użyć funkcji event.traget i mapPropsToHandler.
źródło
Oto moja prosta lista ulubionych produktów zaimplementowana z pisaniem na maszynie React i Redux. Możesz przekazać wszystkie potrzebne argumenty w niestandardowej obsłudze i zwrócić nową,
EventHandler
która akceptuje argument zdarzenia pochodzenia. To jestMouseEvent
w tym przykładzie.Wyizolowane funkcje utrzymują jsx w czystości i zapobiegają łamaniu kilku zasad lintingu. Takie jak
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}>×</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);
Oto fragment kodu javascript, jeśli nie znasz maszynopisu:
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}>×</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);
źródło
Podobnie jak w przypadku składnika bezstanowego, wystarczy dodać funkcję -
function addName(){ console.log("name is added") }
aw zamian nazywany jest jako
onChange={addName}
źródło
Jeśli masz tylko kilka funkcji w swoich rekwizytach, o które się martwisz, możesz to zrobić:
let _dispatch = () => {}; const myHandler = (e) => _dispatch(something()); const myComponent = (props) => { if (!_dispatch) _dispatch = props.dispatch; return ( <button onClick={myHandler}>Click Me</button> ); }
Jeśli robi się to znacznie bardziej skomplikowane, zwykle wracam do komponentu klasowego.
źródło
Po ciągłym wysiłku w końcu u mnie zadziałało.
//..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> );
Uwaga: dla powiązania pola tekstowego atrybut "nazwa" i "nazwa właściwości" (np. Model.name) powinny być takie same, wtedy będzie działać tylko "onInputTextChange". Logikę „onInputTextChange” można modyfikować za pomocą kodu.
źródło
Co powiesz na coś takiego:
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> ); }
ale naprawdę jest to przesada, jeśli nie musisz przekazywać onClick do niższych / wewnętrznych komponentów, jak w przykładzie.
źródło