Maak een React-applicatie helemaal opnieuw (Deel 7): React en Best Practices instellen

Dit bericht maakt deel uit van een reeks berichten op beginnersniveau die bedoeld zijn voor mensen die gebruiken
kant-en-klare tools, sjablonen of boilerplaten voor React, maar willen vanaf het begin leren en begrijpen hoe een React-applicatie te bouwen.

Alle berichten in deze serie:
Deel 1: Introductie
Deel 2: initialisatie en het eerste bestand
Deel 3: Gebruik van de ES2015-syntaxis
Deel 4: Een stijlgids afdwingen
Deel 5: een Express-server instellen
Deel 6: Een modulebundler gebruiken
Deel 7: React en best practices instellen
Deel 8: Redux instellen
Deel 9: React Router instellen
Deel 10: TDD en Jest instellen

React instellen

In dit bericht gaan we React instellen en een zeer eenvoudige component maken, waarna we enkele van de best practices doorlopen die u in gedachten moet houden bij het ontwikkelen van React-componenten. Dit is waar het leuke gedeelte begint, dus laten we er meteen in graven!

Installeer React en React Dom-pakketten als afhankelijkheden:

$ npm installatie - bewaar react react-dom

Open vervolgens index.js en maak een zeer eenvoudige React-component die onze toepassing vertegenwoordigt. We hebben al een element met ID-app in ons index.pug-sjabloonbestand, dus laten we het gebruiken om de applicatie te koppelen.

/ **
 * index.js
 * /
importeren Reageren van 'reageren';
import {render} van 'react-dom';
const MainApp = () => (
  

Hallo reageer!

);
// geef de app weer
render (, document.getElementById ('app'));

Deze eenvoudige code maakt een stateless functionele component MainApp en monteert deze op een DOM-element met een ID-app. Deze code zou niet onmiddellijk werken en u zou een foutmelding krijgen als u probeert de bundel te bouwen of de server te starten.

De reden voor deze fout is dat we JSX-syntaxis in ons bestand index.js hebben die Babel nog niet begrijpt. Om Babel deze syntaxis naar normaal JavaScript te laten interpreteren, gaan we de voorinstelling React voor Babel gebruiken.

Installeer het pakket als een afhankelijkheid:

$ npm installatie - sla babel-preset-react op

Voeg vervolgens de voorinstelling toe aan de lijst met voorinstellingen in .babelrc-bestand:

{
  "presets": [
    "Es2015",
    "Stage-0"
    "Reageer"
  ],
  "plugins": ["transform-inline-environment-variabelen"]
}

Er zou ook een linting-fout moeten zijn waardoor u de bundel niet kunt bouwen. De linter klaagt omdat index.js een JavaScript-bestand is dat JSX-syntaxis bevat maar de extensie js gebruikt in plaats van jsx.

U kunt de beschrijving voor deze regel hier lezen. In het gedeelte 'Wanneer niet gebruiken' staat dat u deze regel niet moet gebruiken als u de extensie van de bestanden met de JSX-syntaxis niet wilt beperken.

Je kunt deze regel blijven gebruiken, maar ik gebruik liever de js-extensie voor alle bestanden, dus ik ga deze regel uitschakelen:

{
  "uitbreiding": "airbnb",
  "env": {
    "es6": waar,
    "browser": waar,
    "node": waar
  },
  "reglement": {
    "react / jsx-bestandsnaam-extensie": 0
  }
}

HMR inschakelen

Het inschakelen van Hot Module-vervanging is net zo eenvoudig als het toevoegen van een codeblok:

/ **
 * index.js
 * /
importeren Reageren van 'reageren';
import {render} van 'react-dom';
if (module.hot) {
  module.hot.accept ();
}
const MainApp = () => (
  

Hallo reageer!

);
// geef de app weer
render (, document.getElementById ('app'));

Tips en praktische tips

Voordat we verdergaan met de zelfstudie, zullen we een geaggregeerde lijst met tips en best practices doorlopen die ik heb geleerd door mijn ervaring met React en ook door lezen en zoeken op internet. Houd daar rekening mee bij het maken van uw React-componenten.

Afhankelijkheidsimport versus lokale import

Scheid de invoer van afhankelijkheid van de lokale invoer door een nieuwe regel. Afhankelijkheid van import moet eerst komen.

import React, {Component} van 'react';
bruikbaarModule importeren uit 'handige-module';
importeer myLocalModule uit './my-local-module';

Stateless functionele componenten

Als de component een component is die alleen wordt weergegeven of geen statusobject hoeft te gebruiken, gebruikt u een gewone JavaScript-functie in plaats van een klasse. Dit wordt een stateless functionele component genoemd.

Dus in plaats van dit te doen:

import React, {Component} van 'react';
class MyComponent breidt component {uit
  render () {
    terug (
      
Hallo!     );   } }
standaard MyComponent exporteren;

Doe dit:

importeren Reageren van 'reageren';
const MyComponent = () => 
Hallo!
;
standaard MyComponent exporteren;

Zien hoeveel rommel is verwijderd? Je kunt het ook eenvoudiger maken door de functie zelf te exporteren:

importeren Reageren van 'reageren';
export default () => 
Hallo!
;

Ik doe dit echter niet de voorkeur omdat het debuggen moeilijker maakt. Als u de React Dev Tools aanvinkt, zult u zien dat de componentnaam ‘Unknown’ is omdat de functie anoniem is.

Anonieme functionele component

Een betere aanpak zou zijn om een ​​normaal genoemde functie te gebruiken in plaats van een anonieme functie:

importeren Reageren van 'reageren';
standaardfunctie exporteren MyComponent () {
  terug 
Hallo!
; }
Benoemde functionele component

Begin met presentatiecomponenten

Presentatiecomponenten zijn eenvoudiger te definiëren, gemakkelijker te begrijpen en kunnen steeds opnieuw worden gebruikt omdat ze onafhankelijk zijn van de rest van de applicatie.

Als u uw toepassing opsplitst in een aantal presentatiecomponenten, kunt u ze allemaal op één pagina plaatsen en hun ontwerp en variaties aanpassen om een ​​uniforme look en feel in de hele toepassing te bereiken.

Bouw uw component als een presentatiecomponent en voeg alleen de status toe wanneer dat nodig is, wat ons naar de volgende tip brengt.

Minimaliseer het gebruik van de staat

Gebruik de staat spaarzaam in uw componenten en zorg ervoor dat ze de staat gebruiken voor de gebruikersinterface in plaats van gegevens, met andere woorden, als u het niet gebruikt in render (), dan zou het niet de status moeten hebben. Vergeet niet dat u setState alleen moet gebruiken als u uw component opnieuw wilt renderen.

Laten we zeggen dat we een component hebben die uit een enkele knop bestaat. Op deze knop kan slechts eenmaal worden geklikt en wanneer erop wordt geklikt, wordt een bericht vastgelegd in de console:

import React, {Component} van 'react';

class MyComponent breidt component {uit
  staat = {
    clickedOnce: false,
  };
  handleClick = () => {
    if (! this.state.clickedOnce) {
      console.log ( 'geklikt');
    }
    this.setState ({
      clickedOnce: true,
    });
  }
  componentDidUpdate () {
    console.log ( 'update!');
  }
  render () {
    terug (
      
                    );   } }
standaard MyComponent exporteren;

Dit is een voorbeeld van een slechte implementatie, waarbij de status wordt gebruikt om een ​​vlag in te stellen waarop eenmaal wordt geklikt, die aangeeft of er opnieuw op de knop kan worden geklikt. Telkens wanneer op de knop wordt geklikt, wordt de component opnieuw gerenderd, hoewel dit niet nodig is.

De toepassing wordt opnieuw weergegeven als u op een knop klikt

Dit zou beter zijn:

import React, {Component} van 'react';

class MyComponent breidt component {uit
  clickedOnce = false;
  
  handleClick = () => {
    if (! this.clickedOnce) {
      console.log ( 'geklikt');
    }
    this.clickedOnce = true;
  }
  componentDidUpdate () {
    console.log ( 'update!');
  }
  render () {
    terug (
      
                    );   } }
standaard MyComponent exporteren;

Deze implementatie gebruikt een klasse-eigenschap in plaats van een statussleutel, omdat de vlag clickedOnce geen UI-status vertegenwoordigt en daarom niet binnen de componentstatus zou moeten leven. Bij deze nieuwe implementatie wordt een update niet meer geactiveerd als u meerdere keren op de knop klikt.

Definieer altijd propTypes en defaultProps

Alle componenten moeten propTypes en defaultProps hebben die zo hoog mogelijk binnen de component zijn gedefinieerd. Ze dienen als componentdocumentatie en moeten onmiddellijk zichtbaar zijn voor andere ontwikkelaars die het bestand lezen.

Sinds React v15.5 is React.PropTypes naar een ander pakket verhuisd, dus laten we dat pakket als afhankelijkheid installeren:

$ npm installatie - red prop-types

Voor staatloze functionele componenten:

Functies worden gehesen in JavaScript, wat betekent dat u een functie kunt gebruiken vóór de verklaring:

importeren Reageren van 'reageren';
PropTypes importeren uit 'prop-types';
MyComponent.propTypes = {
  titel: PropTypes.string,
};
MyComponent.defaultProps = {
  titel: 'Een eenvoudige teller',
};
standaardfunctie exporteren MyComponent (rekwisieten) {
  retourneer 

{props.title}

; }

ESLint zou klagen over het gebruik van de functie vóór de definitie, maar laten we omwille van betere componentdocumentatie deze linting-regel voor functies uitschakelen door het .eslintrc-bestand te wijzigen:

{
  ...
  "reglement": {
    "react / jsx-bestandsnaam-extensie": 0,
    "no-use-before-define": [
      "fout",
      {
        "functies": onwaar
      }
    ]
  }
}

Voor op componenten gebaseerde componenten:

In tegenstelling tot functies worden klassen in JavaScript niet gehesen, dus we kunnen niet eenvoudig MyComponent.propTypes = ... doen voordat we de klasse zelf definiëren, maar we kunnen propTypes en defaultProps definiëren als statische klasse-eigenschappen:

import React, {Component} van 'react';
PropTypes importeren uit 'prop-types';
class MyComponent breidt component {uit
  statische propTypes = {
    titel: PropTypes.string,
  };
  static defaultProps = {
    titel: 'Een eenvoudige teller',
  };
  render () {
    retourneer 

{this.props.title}

;   } }
standaard MyComponent exporteren;

De staat initialiseren

Status kan worden geïnitialiseerd binnen de componentconstructor:

class MyComponent breidt component {uit
  constructor (props) {
    super (stutten);
    this.state = {
      tel: 0,
    };
  }
}

Een betere manier is om de staat te initialiseren als klasse-eigenschap:

class MyComponent breidt component {uit
  constructor (props) {
    super (stutten);
  }
  staat = {
    tel: 0,
  };
}

Dit ziet er veel beter, schoner en beter leesbaar uit en draagt ​​ook bij aan de documentatie van componenten. Het statusobject moet worden geïnitialiseerd na propTypes en defaultProps:

import React, {Component} van 'react';
PropTypes importeren uit 'prop-types';
class MyComponent breidt component {uit
  // propTypes komt eerst
  statische propTypes = {
    titel: PropTypes.string,
  };
  // defaultProps komt op de tweede plaats
  static defaultProps = {
    titel: 'Een eenvoudige teller',
  };
  // constructeur komt hier
  constructor () {
    ...
  }
  // dan komt de staat
  staat = {
    tel: 0,
  };
}

Geef een functie door aan setState

React-documentatie ontmoedigt het vertrouwen op this.state en this.props om de volgende status te berekenen, omdat React ze asynchroon bijwerkt. Dat betekent dat de status mogelijk niet onmiddellijk verandert nadat setState () is aangeroepen.

class MyComponent breidt component {uit
  staat = {
    tellen: 10,
  }
  onClick = () => {
    console.log (this.state.count); // 10
    
    // aantal zal niet onmiddellijk veranderen
    this.setState ({count: this.state.count + this.props.step});
    
    console.log (this.state.count); // nog steeds 10
  }
}

Hoewel dit werkt voor eenvoudige scenario's en de status nog steeds correct wordt bijgewerkt, kan dit leiden tot onverwacht gedrag in complexere scenario's.

Overweeg dit scenario, u hebt een component die een enkele knop weergeeft. Wanneer u op deze knop klikt, wordt een handleClick-methode genoemd:

class MyComponent breidt component {uit
  static defaultProps = {
    stap: 5,
  }
  statische propTypes = {
    stap: PropTypes.number,
  }
  
  staat = {
    tellen: 10,
  }
  
  handleClick = () => {
    this.doSomething ();
    this.doSomethingElse ();
  }
  doSomething = () => {
    this.setState ({count: this.state.count + this.props.step});
  }
  doSomethingElse = () => {
    this.setState ({count: this.state.count - 1});
  }
  render () {
    terug (
      
        

Huidige telling is: {this.state.count}

                    );   } }

De knop roept handleClick () aan wanneer erop wordt geklikt, wat op zijn beurt doSomething () en vervolgens doSomethingElse () aanroept. Beide functies zullen de telwaarde binnen de status wijzigen.

Logisch gezien is 10 + 5 15 en dan 1 aftrekken en het resultaat moet 14 zijn, toch? In dit geval is dat niet het geval - de waarde van count na de eerste klik is 9, niet 14. Dit gebeurt omdat de waarde van this.state.count nog steeds 10 is wanneer doSomethingElse () wordt genoemd, niet 15.

Om dit op te lossen, kunt u een tweede vorm van setState () gebruiken die een functie accepteert in plaats van een object. Die functie ontvangt de vorige status als het eerste argument en de rekwisieten op het moment dat de update wordt toegepast als het tweede argument:

this.setState ((prevState, props) => ({
  count: prevState.count + props.step
}))

We kunnen dit formulier gebruiken om ons voorbeeld te verbeteren:

class MyComponent breidt component {uit
  ...
  handleClick = () => {
    this.doSomething ();
    this.doSomethingElse ();
  }
  doSomething = () => {
    this.setState ((prevState, props) => ({
      count: prevState.count + props.step
    }));
  }
  doSomethingElse = () => {
    this.setState (prevState => ({
      count: prevState.count - 1
    }));
  }
  ...
}

Met deze implementatie wordt de telling correct bijgewerkt van 10 tot 14 tot 18 enzovoort. Eenvoudige wiskunde is weer zinvol!

Gebruik pijlfuncties als klasse-eigenschappen

Het trefwoord this is altijd verwarrend geweest voor JavaScript-ontwikkelaars en het gedrag is niet minder verwarrend in React-componenten. Weet u hoe dit trefwoord verandert in een React-component? Overweeg het volgende voorbeeld:

import React, {Component} van 'react';
class MyComponent breidt component {uit
  staat = {
    tel: 0,
  };
  bij klikken() {
    console.log (this.state);
  }
  render () {
    terug (
      
        

Aantal is: {this.state.count}

                    );   } }
standaard MyComponent exporteren;

Klikken op de knop zou een fout veroorzaken:

Uncaught TypeError: Kan eigenschap ‘staat’ van undefined niet lezen

Dit komt omdat onClick, als klassemethode, niet standaard is gebonden. Er zijn een paar manieren om dit te corrigeren. (bedoelde woordspeling, snap je?)

Een manier is om de functie aan de juiste context te binden terwijl u deze binnen de render () -functie doorgeeft:

Of u kunt voorkomen dat de context wordt gewijzigd door een pijlfunctie te gebruiken in render ():

Deze twee methoden hebben echter een lichte prestatiekost omdat de functie bij elke weergave opnieuw wordt toegewezen. Om deze kleine prestatiekosten te voorkomen, kunt u de functie in de constructor binden:

import React, {Component} van 'react';
class MyComponent breidt component {uit
  constructor (props) {
    super (stutten);
    this.onClick = this.onClick.bind (this);
  }
  ...
  render () {
    ...
    
    ...
  }
}
standaard MyComponent exporteren;

Deze techniek is beter, maar je kunt je gemakkelijk laten meeslepen en eindigen met iets dat er zo uitziet:

constructor (props) {
  // dit is slecht, echt slecht
  this.onClick = this.onClick.bind (this);
  this.onChange = this.onChange.bind (this);
  this.onSubmit = this.onSubmit.bind (this);
  this.increaseCount = this.increaseCount.bind (this);
  this.decreaseCount = this.decreaseCount.bind (this);
  this.resetCount = this.resetCount.bind (this);
  ...
}

Omdat we Babel gebruiken en ondersteuning voor klasse-eigenschappen hebben, is de betere manier om pijlfuncties te gebruiken bij het definiëren van de klassenmethoden:

class MyComponent breidt component {uit
  ...
  onClick = () => {
    // 'dit' wordt bewaard
    console.log (this.state);
  }
  render () {
    terug (
      
        

{this.state.count}                     );   } }

Vernietig het rekwisietenobject

Wanneer een component veel rekwisieten heeft, vernietigt u het rekwisietenobject en plaatst u elke eigenschap op zijn eigen regel.

Voor staatloze functionele componenten:

standaardfunctie exporteren MyComponent ({
  Voornaam,
  achternaam,
  e-mailadres,
  Omschrijving,
  onChange,
  onSubmit,
}) {
  terug (
    
      

{firstName}       ...        ); }

Standaardargumenten zijn geen excuus om defaultProps te laten vallen. Zoals eerder vermeld, moet u altijd propTypes en defaultProps definiëren.

Voor op componenten gebaseerde componenten:

class MyComponent breidt component {uit
  ...
  render () {
    const {
      Voornaam,
      achternaam,
      e-mailadres,
      Omschrijving,
      onChange,
      onSubmit,
    } = this.props;
    terug (
      
        

{firstName}         ...            );   } }

Dit is schoner, maakt het eenvoudiger om eigenschappen opnieuw te ordenen en maakt het gemakkelijker om eigenschappen aan / uit de lijst toe te voegen / te verwijderen terwijl een leesbaar verschil voor Git wordt gegenereerd. Stel je de volgende situatie voor:

Verschil is beter leesbaar wanneer elke eigenschap zich op een nieuwe regel bevindt

Aan de rechterkant kun je gemakkelijk zien welke eigenschap is toegevoegd, aan de linkerkant weet je echter alleen dat er iets aan die regel is veranderd en moet je heel goed kijken om uit te vinden welk deel van de regel veranderd.

Voorwaardelijke weergave

Wanneer u een van de twee componenten of blokken JSX-code moet renderen op basis van een voorwaarde, gebruikt u een ternaire expressie:

isLoggedIn
  ? 
Welkom, {gebruikersnaam}!
  :

Als de code uit meerdere regels bestaat, gebruikt u haakjes:

isLoggedIn? (
  
    Welkom, {gebruikersnaam}!    ): (   

Als u een enkel onderdeel of een blok JSX-code op basis van een voorwaarde moet renderen, gebruik dan een evaluatie van de kortsluiting:

isComplete && 
Je bent klaar!

Gebruik haakjes voor meer dan één regel:

is compleet && (
  
    Je bent klaar!    )

Het belangrijkste kenmerk

Het is een gebruikelijk patroon om Array.prototype.map en vergelijkbare matrixmethoden te gebruiken in de functie render () en het sleutelattribuut is gemakkelijk te vergeten. Verzoening is moeilijk genoeg, maak het niet moeilijker. Vergeet niet om de sleutel te plaatsen waar deze hoort. (woordspeling opnieuw bedoeld, snap je?)

render () {
  terug (
    
    {       {         items.map (item => (           
  •             {Itemnaam}                    ))        }        ); }

Wanneer u map (), filter () of vergelijkbare matrixmethoden gebruikt, is de tweede parameter voor de callback de index van het item. Het is over het algemeen een slecht idee om deze index als sleutel te gebruiken. React gebruikt het sleutelkenmerk om te identificeren welke items zijn gewijzigd, toegevoegd of verwijderd en de sleutelwaarde moet stabiel zijn en elk item uniek identificeren.

In gevallen waarin de array wordt gesorteerd of een element wordt toegevoegd aan het begin van de array, wordt de index gewijzigd, ook al kan het element dat die index vertegenwoordigt hetzelfde zijn. Dit resulteert in onnodige weergave en in sommige gevallen resulteert het in het weergeven van verkeerde gegevens.

Gebruik UUID of ShortID om een ​​uniek ID voor elk item te genereren wanneer het voor het eerst wordt gemaakt en gebruik het als de sleutelwaarde.

Normaliseer de staat

Probeer het statusobject te normaliseren en zo vlak mogelijk te houden. Gegevens in de status nesten betekent dat er complexere logica nodig is om deze bij te werken. Stel je voor hoe lelijk het zou zijn om een ​​diep genest veld bij te werken - wacht, stel je niet voor, hier:

// het statusobject
staat = {
  ...
  berichten: [
    ...,
    {
      meta: {
        id: 12,
        auteur: '...',
        public: false,
        ...
      },
      ...
    },
    ...
  ],
  ...
};
// om 'openbaar' bij te werken naar 'waar'
this.setState ({
  ... this.state,
  berichten: [
    ... this.state.posts.slice (0, index),
    {
      ... this.state.posts [index]
      meta: {
        ... this.state.posts [index] .meta,
        publiek: waar,
      }
    },
    ... this.state.posts.slice (index + 1),
  ]
});

Deze code is niet alleen lelijk, maar kan ook ertoe leiden dat niet-gerelateerde componenten opnieuw worden weergegeven, zelfs als de gegevens die ze weergeven niet daadwerkelijk zijn gewijzigd. Dit komt omdat we alle voorouders in de statusboom hebben bijgewerkt met nieuwe objectverwijzingen.

Het bijwerken van hetzelfde veld zou eenvoudiger zijn als we de statusboom opnieuw structureren:

staat = {
  berichten: [
    10,
    11,
    12,
  ],
  meta: {
    10: {
      id: 10,
      auteur: 'author-a',
      public: false,
    },
    11: {
      id: 11,
      auteur: 'author-b',
      public: false,
    },
    12: {
      id: 12,
      auteur: 'author-c',
      public: false,
    },
  },
}
this.setState ({
  meta: {
    ... this.state.meta,
    [ID kaart]: {
      ... this.state.meta [id],
      publiek: waar,
    }
  }
})

Gebruik klassennamen

Als je ooit in een situatie bent geweest waarin je een voorwaardelijke klassenaam moet gebruiken, heb je misschien iets geschreven dat er zo uitziet:

  ...

Dit wordt echt lelijk als je meerdere voorwaardelijke klassennamen hebt. Gebruik in plaats van een ternaire te gebruiken het pakket met klassennamen:

klassennamen importeren uit 'klassennamen';
const classes = classnames ('tab', {'is-active': isActive});
  ...

Eén component per bestand

Dit is geen harde regel, maar ik geef er de voorkeur aan om elk onderdeel in één bestand te houden en dat onderdeel standaard te exporteren. Ik heb hier geen specifieke reden voor, maar ik vind het gewoon netter en overzichtelijker - high five als je dat ook doet!

Gevolgtrekking

Er is geen goede manier om een ​​React-component te ontwikkelen. We hebben een aantal van de best practices en patronen doorlopen die u zullen helpen herbruikbare en onderhoudbare componenten te ontwikkelen en ik ben erg benieuwd naar wat voor soort patronen en practices u verkiest en waarom.

In het volgende deel van deze serie gaan we een eenvoudige To-Do-applicatie bouwen en enkele van deze best practices en patronen toepassen.

Was dit artikel nuttig? Klik hieronder op de Clap-knop of volg me voor meer informatie.

Bedankt voor het lezen! Als je feedback hebt, laat dan een reactie achter.

Ga naar deel 7-b: een eenvoudige ToDo-app bouwen (binnenkort)