Dlaczego rekwizyty JSX nie powinny używać funkcji arrow lub bind?

Uruchamiam lint z moją aplikacją Reactową i otrzymuję ten błąd:

error    JSX props should not use arrow functions        react/jsx-no-bind

I tu uruchamiam funkcję strzałki (wewnątrz onClick):

{this.state.photos.map(tile => (
  <span key={tile.img}>
    <Checkbox
      defaultChecked={tile.checked}
      onCheck={() => this.selectPicture(tile)}
      style={{position: 'absolute', zIndex: 99, padding: 5, backgroundColor: 'rgba(255, 255, 255, 0.72)'}}
    />
    <GridTile
      title={tile.title}
      subtitle={<span>by <b>{tile.author}</b></span>}
      actionIcon={<IconButton onClick={() => this.handleDelete(tile)}><Delete color="white"/></IconButton>}
    >
      <img onClick={() => this.handleOpen(tile.img)} src={tile.img} style={{cursor: 'pointer'}}/>
    </GridTile>
  </span>
))}
Czy jest to zła praktyka, której należy unikać? A jak najlepiej to zrobić?
Author: KadoBOT, 2016-04-17

6 answers

Dlaczego nie powinieneś używać funkcji strzałek inline w rekwizytach JSX

Używanie funkcji strzałek lub wiązania W JSX jest złą praktyką, która szkodzi wydajności, ponieważ funkcja jest odtwarzana przy każdym renderowaniu.

  1. Za każdym razem, gdy funkcja jest tworzona, poprzednia funkcja jest zbierana śmieci. Ponowne nadawanie wielu elementów może tworzyć jankesy w animacjach.

  2. Użycie funkcji strzałki w linii spowoduje PureComponents, a składniki, które używają shallowCompare w shouldComponentUpdate metoda na rerender mimo wszystko. Ponieważ rekwizytor funkcji strzałki jest odtwarzany za każdym razem, płytkie porównanie zidentyfikuje go jako zmianę na rekwizytor, a komponent będzie ponownie odtwarzał.

Jak widać na poniższych przykładach - gdy używamy funkcji strzałek w linii, komponent <Button> jest ponownie wyświetlany za każdym razem (konsola pokazuje tekst "render button").

przykład 1-PureComponent bez inline handler

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  onClick = () => this.setState((prevState) => ({
    counter: prevState.counter + 1
  }));
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ this.onClick } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

przykład 2-PureComponent with inline handler

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ () => this.setState((prevState) => ({
          counter: prevState.counter + 1
        })) } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Wiązanie metod do this BEZ inlining funkcji strzałek

  1. Powiązanie metody ręcznie w konstruktorze:

    class Button extends React.Component {
      constructor(props, context) {
        super(props, context);
    
        this.cb = this.cb.bind(this);
      }
    
      cb() {
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    
  2. Wiązanie metody wykorzystującej proposal-class-fields z funkcją strzałki. Ponieważ jest to propozycja etapu 3, musisz dodać w zależności od tego, która z tych wartości jest większa, można ją przekształcić w bibliotekę babel.

    class Button extends React.Component {
      cb = () => { // the class property is initialized with an arrow function that binds this to the class
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    

Komponenty funkcyjne z wewnętrznymi wywołaniami

Kiedy tworzymy wewnętrzną funkcję (na przykład obsługę zdarzeń) wewnątrz komponentu funkcyjnego, funkcja będzie odtwarzana za każdym razem, gdy komponent jest renderowany. Jeśli funkcja zostanie przekazana jako Właściwości (lub poprzez kontekst) do komponentu potomnego (Button w tym przypadku), to dziecko ponownie wyrenderuje jako cóż.

przykład 1-element funkcyjny z wewnętrznym wywołaniem zwrotnym:

const { memo, useState } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); // the function is recreated all the time
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Aby rozwiązać ten problem, możemy zawinąć callback z useCallback() hook i ustaw zależności na pustą tablicę.

Uwaga: wygenerowana funkcja useState akceptuje funkcję updater, która dostarcza bieżący stan. W ten sposób nie musimy ustawiać aktualnego stanu jako zależności useCallback.

przykład 2 - Komponent funkcyjny z wewnętrznym wywołaniem zwrotnym owiniętym useCallback:

const { memo, useState, useCallback } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
 178
Author: Ori Drori,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-11 16:57:52

Dzieje się tak, ponieważ funkcja arrow najwyraźniej utworzy nową instancję funkcji przy każdym renderowaniu, jeśli zostanie użyta we właściwości JSX. Może to spowodować ogromne obciążenie dla garbage collector, a także utrudni przeglądarce optymalizację wszelkich "gorących ścieżek", ponieważ funkcje zostaną wyrzucone zamiast ponownie wykorzystane.

Możesz zobaczyć całe wyjaśnienie i więcej informacji na https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md

 9
Author: Karl-Johan Sjögren,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-17 14:28:00

Używanie takich funkcji inline jest w porządku. Zasada lintingu jest przestarzała.

Ta zasada pochodzi z czasów, gdy funkcje strzałek nie były tak powszechne, a ludzie używali.bind (this), który kiedyś był powolny. Problem z wydajnością został rozwiązany w Chrome 49.

Zwróć uwagę, że nie przekazujesz funkcji inline jako właściwości do komponentu podrzędnego.

Ryan Florence, autor routera Reactowego, napisał świetny artykuł o to:

Https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578

 6
Author: sbaechler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-02-16 22:18:17

Aby uniknąć tworzenia nowych funkcji z tymi samymi argumentami, możesz zapamiętać wynik bind funkcji, oto proste narzędzie o nazwie memobind aby to zrobić: https://github.com/supnate/memobind

 4
Author: supNate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-14 03:23:28

Dlaczego rekwizyty JSX nie powinny używać funkcji arrow lub bind?

Głównie dlatego, że funkcje wbudowane mogą złamać memoizację zoptymalizowanych komponentów:

Tradycyjnie, problemy z wydajnością związane z funkcjami wbudowanymi w Reacta były związane z przekazywaniem nowych wywołań zwrotnych przy każdym renderowaniu shouldComponentUpdate optymalizacji w komponentach potomnych. (docs )

Mniej o dodatkowe koszty tworzenia funkcji:

Problemy z wydajnością Function.prototype.bind got fixed here and arrow functions are or a native thing or are transpiled by babel to plain functions; in both cases we can assume it ' s not slow. (React Training )

Uważam, że ludzie, którzy twierdzą, że tworzenie funkcji jest kosztowne, zawsze byli wprowadzani w błąd(React team nigdy tego nie powiedział). (Tweet )

Kiedy zasada react/jsx-no-bind jest przydatna?

Chcesz mieć pewność, że zapamiętane komponenty działają zgodnie z przeznaczeniem:

  • React.memo (dla funkcji komponenty)
  • PureComponent lub niestandardowe shouldComponentUpdate (Dla komponentów klasy)

Przestrzegając tej reguły, przekazywane są odniesienia do stabilnych obiektów funkcji. Powyższe komponenty mogą zoptymalizować wydajność, zapobiegając ponownemu renderowaniu, gdy poprzednie rekwizyty nie uległy zmianie.

Jak rozwiązać błąd ESLint?

Klasy: definiują obsługę jako metodę, lub Właściwość klasy dla wiązania this.
Haki: Użyj useCallback.

Middleground

W wielu przypadkach, funkcje inline są bardzo wygodne w użyciu i absolutnie doskonałe pod względem wymagań dotyczących wydajności. Niestety, zasada ta nie może ograniczać się tylko do zapamiętanych typów komponentów. Jeśli nadal chcesz używać go w całej sieci, możesz np. wyłączyć go dla prostych węzłów DOM:

rules: {
  "react/jsx-no-bind": [ "error", { ignoreDOMComponents: true } ],
}

const Comp = () => <span onClick={() => console.log("Hello!")} />; // no warning
 1
Author: ford04,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Możesz używać funkcji strzałek używając biblioteki react-cached-handler , nie musisz się martwić o wydajność ponownego renderowania:

Uwaga: wewnętrznie buforuje funkcje strzałek za pomocą określonego klawisza, nie musisz się martwić o ponowne renderowanie!

render() {
    return (
        <div>
            {this.props.photos.map((photo) => (
                <Photo
                    key={photo.url}
                    onClick={this.handler(photo.url, (url) => {
                        console.log(url);
                    })}
                />
            ))}
        </div>
    );
}

Inne cechy:

  • nazwani handlerzy
  • Obsługa zdarzeń za pomocą funkcji strzałek
  • dostęp do klucza, argumentów niestandardowych i oryginalnego zdarzenia
  • renderowanie komponentów performace
  • Custom context for handlers
 -1
Author: Ghominejad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-22 11:51:26