javascript - Varför borde inte JSX-props använda arrow-funktioner eller bind?
Jag kör lint på min React-app och får detta fel:
error JSX props should not use arrow functions react/jsx-no-bind
Och här är där jag kör pilfunktionen (inne i 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>
))}
Är detta en dålig praxis som bör undvikas? Och vad är det bästa sättet att göra det?
Svar #1
Varför du inte bör använda inbyggda pilfunktioner i JSX-egenskaper
Att använda pilfunktioner eller bindning i JSX är en dålig praxis som skadar prestandan, eftersom funktionen återskapas vid varje rendering.
När en funktion skapas, tas den tidigare funktionen bort av skräpinsamlingen. Att återrendera många element kan skapa hack i animationer.
Att använda en inline pilfunktion kommer att göra att
PureComponents, och komponenter som användershallowCompareishouldComponentUpdate-metoden, ändå renderas om. Eftersom pilfunktions-propen återskapas varje gång kommer den ytliga jämförelsen att identifiera den som en förändring av en prop, och komponenten kommer att renderas om.
Som du kan se i de följande 2 exemplen - när vi använder inline-funktion med pil, renderas <Button>-komponenten om varje gång (konsolen visar texten 'render button').
Exempel 1 - PureComponent utan inline-hanterare
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>Exempel 2 - PureComponent med inline-hanterare
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>Binda metoder till this utan att inlinea arrow-funktioner
Binder metoden manuellt i konstruktören:
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> ); } }Bindning av en metod med förslags-klassfält med en pilfunktion. Eftersom detta är ett förslag i steg 3, behöver du lägga till Stage 3-preset eller Class properties-transformen till din babel-konfiguration.
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> ); } }
Funktionskomponenter med inre callbacks
När vi skapar en inre funktion (till exempel en händelsehanterare) inuti en funktionskomponent, kommer funktionen att återskapas varje gång komponenten renderas. Om funktionen skickas som props (eller via context) till en barnkomponent (Button i det här fallet), kommer även den barnet att renderas om.
Exempel 1 - Funktionskomponent med en intern återuppringning:
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>För att lösa detta problem kan vi omsluta callbacken med useCallback()-hooken och sätta beroendena till en tom array.
Observera: den useState genererade funktionen accepterar en uppdateringsfunktion som tillhandahåller det aktuella tillståndet. På detta sätt behöver vi inte sätta det aktuella tillståndet som en beroende av useCallback.
Exempel 2 - Funktionskomponent med en inre callback inlindad med 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>Svar #2
Att använda inline-funktioner på det här sättet är helt okej. Lint-regeln är föråldrad.
Denna regel kommer från en tid då pilfunktioner inte var lika vanliga och folk använde .bind(this), vilket brukade vara långsamt. Prestandaproblemet har åtgärdats i Chrome 49.
Var noga med att du inte skickar inline-funktioner som props till en barnkomponent.
Ryan Florence, författaren till React Router, har skrivit ett fantastiskt inlägg om detta:
https://reacttraining.com/blog/react-inline-functions-and-performance/
Svar #3
Varför borde inte JSX-props använda arrow-funktioner eller bind?
Främst eftersom inline-funktioner kan bryta memoization av optimerade komponenter:
Traditionellt har prestandaangelägenheter kring inline-funktioner i React varit relaterade till hur att skicka nya callbacks vid varje render bryter
shouldComponentUpdate-optimeringar i barnkomponenter. (docs)
Det handlar mindre om kostnaden för skapande av ytterligare funktioner:
Prestandaproblem med
Function.prototype.bindåtgärdades här och pilfunktioner är antingen ett nativerat fenomen eller transpilas av Babel till vanliga funktioner; i båda fallen kan vi anta att det inte är långsamt. (React Training)Jag tror att folk som påstår att funktionsskapande är dyrt alltid har varit felinformerade (React-teamet har aldrig sagt detta). (Tweet)
När är regeln react/jsx-no-bind användbar?
Du vill säkerställa att memoiserade komponenter fungerar som avsett:
React.memo(för funktionskomponenter)PureComponenteller anpassadshouldComponentUpdate(för klasskomponenter)
Genom att följa denna regel skickas stabila referenser till funktionsobjekt. Så ovanstående komponenter kan optimera prestanda genom att förhindra omrenderingar när tidigare egenskaper inte har ändrats.
Hur löser man ESLint-felet?
Klasser: Definiera handhavaren som metod eller klass-egenskap för this-bindning.
Hooks: Använd useCallback.
Mellangrund
I många fall är inline-funktioner mycket bekväma att använda och helt okej när det gäller prestandakrav. Tyvärr kan denna regel inte begränsas till endast memoiserade komponenttyper. Om du fortfarande vill använda det generellt, kan du t.ex. inaktivera det för enkla DOM-noder:
rules: {
"react/jsx-no-bind": [ "error", { "ignoreDOMComponents": true } ],
}
const Comp = () => <span onClick={() => console.log("Hello!")} />; // no warning
Svar #4
Detta beror på att en pilfunktion uppenbarligen kommer att skapa en ny instans av funktionen vid varje renderering om den används i en JSX-egenskap. Detta kan skapa en stor belastning på skräpinsamlaren och kommer också att hindra webbläsaren från att optimera några "heta spår" eftersom funktioner kommer att kastas bort istället för att återanvändas.
Du kan se hela förklaringen och lite mer information på https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md
