Opret en React-applikation fra bunden (Del 7): Opsætning af React og bedste praksis

Dette indlæg er en del af en række stillinger, der er beregnet til folk, der bruger
færdige værktøjer, skabeloner eller kedelplader til React, men ønsker at lære og forstå, hvordan man bygger en React-applikation helt fra begyndelsen.

Alle indlæg i denne serie:
Del 1: Introduktion
Del 2: Initialisering og den første fil
Del 3: Brug af ES2015 Syntax
Del 4: Håndhævelse af en stilguide
Del 5: Opsætning af en Express Server
Del 6: Brug af en modulbundter
Del 7: Opsætning af reaktion og bedste praksis
Del 8: Opsætning af Redux
Del 9: Opsætning af React Router
Del 10: TDD og Opsætning af Jest

Opsætning af React

I dette indlæg skal vi opsætte React og oprette en meget enkel komponent, så gennemgår vi nogle af de bedste fremgangsmåder, som du skal huske på, når du udvikler React-komponenter. Det er her den sjove del starter, så lad os grave lige ind!

Installer React og React Dom-pakker som afhængigheder:

$ npm installation - gem reagerer reaktionsdom

Åbn derefter index.js og opret en meget enkel React-komponent, der repræsenterer vores applikation. Vi har allerede et element med id-app i vores index.pug-skabelonfil, så lad os bruge det til at montere applikationen.

/ **
 * index.js
 * /
import React fra 'react';
import {render} fra 'react-dom';
const MainApp = () => (
  

Hejreaktion!

);
// gengiv appen
render (, document.getElementById ('app'));

Denne enkle kode opretter en statsløs funktionel komponent MainApp og monterer den på et DOM-element, der har en id-app. Denne kode fungerer ikke med det samme, og du får en fejl, hvis du prøver at bygge bundtet eller starte serveren.

Årsagen til denne fejl er, at vi har JSX-syntaks i vores index.js-fil, som Babel endnu ikke forstår. For at give Babel mulighed for at fortolke denne syntaks til normal JavaScript, skal vi bruge React-forudindstillingen til Babel.

Installer pakken som en afhængighed:

$ npm installation - gem babel-forudindstillet-reaktion

Føj derefter forudindstillingen til listen med forudindstillinger i .babelrc-fil:

{
  "forudindstillinger": [
    "Es2015",
    "Fase-0",
    "reagere"
  ],
  "plugins": ["transform-inline-miljø-variabler"]
}

Der skal også være en fnugefejl, der forhindrer dig i at opbygge bundtet. Linteren klager, fordi index.js er en JavaScript-fil, der indeholder JSX-syntaks, men bruger js-udvidelsen i stedet for jsx.

Du kan læse beskrivelsen til denne regel her. Afsnittet 'Når man ikke skal bruge det' siger, at du ikke bør bruge denne regel, hvis du ikke er interesseret i at begrænse udvidelsen af ​​de filer, der indeholder JSX-syntaks.

Du kan fortsætte med at bruge denne regel, men jeg foretrækker at bruge js-udvidelsen til alle filer, så jeg vil deaktivere denne regel:

{
  "extends": "airbnb",
  "env": {
    "es6": sandt,
    "browser": sandt,
    "node": sandt
  },
  "regler": {
    "react / jsx-filename-extension": 0
  }
}

Aktiverer HMR

At aktivere udskiftning af hotmodul er så simpelt som at tilføje en blok med kode:

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

Hejreaktion!

);
// gengiv appen
render (, document.getElementById ('app'));

Tip og bedste praksis

Inden vi fortsætter med tutorial, vil vi gennemgå en samlet liste over tip og bedste praksis, som jeg har lært af min erfaring med React og også fra at læse og søge på nettet. Husk dem, når du opretter dine React-komponenter.

Afhængighedsimport kontra lokalimport

Adskil afhængighedsimporten fra den lokale import med en ny linje. Afhængighedsimport bør komme først.

import React, {Component} fra 'react';
import nyttigeModule fra 'nyttigt modul';
import myLocalModule fra './my-local-module';

Statsløse funktionelle komponenter

Hvis komponenten er en gengivelseskomponent eller ikke behøver at bruge et tilstandsobjekt, skal du bruge en almindelig JavaScript-funktion i stedet for en klasse. Dette kaldes en statsløs funktionel komponent.

Så i stedet for at gøre dette:

import React, {Component} fra 'react';
klasse MyComponent udvider komponent {
  render () {
    Vend tilbage (
      
Hej!     );   } }
eksporter standard MyComponent;

Gør dette:

import React fra 'react';
const MyComponent = () => 
Hej!
;
eksporter standard MyComponent;

Se hvor meget rod der blev fjernet? Du kan også gøre det enklere ved at eksportere selve funktionen:

import React fra 'react';
eksport standard () => 
Hej!
;

Jeg foretrækker dog ikke at gøre dette, fordi det gør debugging sværere. Hvis du kontrollerer React Dev Tools, vil du se, at komponentnavnet er 'Ukendt', fordi funktionen er anonym.

Anonym funktionel komponent

En bedre fremgangsmåde ville være at bruge en normal navngivet funktion i stedet for en anonym funktion:

import React fra 'react';
eksport standardfunktion MyComponent () {
  return 
Hej!
; }
Navngivet funktionel komponent

Start med præsentationskomponenter

Præsentationskomponenter er enklere at definere, lettere at forstå og kan genbruges igen og igen, fordi de er uafhængige af resten af ​​applikationen.

Hvis du bryder din applikation ned til et sæt præsentative komponenter, kan du placere dem alle på en enkelt side og finpusse deres design og variationer for at opnå et samlet udseende og fornemmelse i hele applikationen.

Byg din komponent som en præsentationskomponent og tilføj kun tilstand, når du har brug for det, hvilket bringer os til det næste tip.

Minimer brug af staten

Brug tilstand sparsomt i dine komponenter, og sørg for, at de bruger tilstand til UI snarere end data, med andre ord, hvis du ikke bruger den i render (), skulle den ikke være i staten. Husk, at du kun skal bruge setState, hvis du vil gendanne din komponent igen.

Lad os sige, at vi har en komponent, der består af en enkelt knap. Der kan kun klikkes på denne knap én gang, og når der klikkes på den, logges en meddelelse på konsollen:

import React, {Component} fra 'react';

klasse MyComponent udvider komponent {
  tilstand = {
    clickedOnce: falsk,
  };
  handleClick = () => {
    hvis (! this.state.clickedOnce) {
      console.log ( 'klikket');
    }
    this.setState ({
      clickedOnce: sandt,
    });
  }
  componentDidUpdate () {
    console.log ( 'Opdateret!');
  }
  render () {
    Vend tilbage (
      
         Klik på mig            );   } }
eksporter standard MyComponent;

Dette er et eksempel på en dårlig implementering ved hjælp af staten til at indstille et klik klikket på en gang, der specificerer, om knappen kan klikkes igen. Hver gang der klikkes på knappen, gengives komponenten igen, selvom den ikke behøver det.

Programmet gengives igen ved at klikke på knappen

Dette ville være bedre:

import React, {Component} fra 'react';

klasse MyComponent udvider komponent {
  clickedOnce = falsk;
  
  handleClick = () => {
    hvis (! this.clickedOnce) {
      console.log ( 'klikket');
    }
    this.clickedOnce = sandt;
  }
  componentDidUpdate () {
    console.log ( 'Opdateret!');
  }
  render () {
    Vend tilbage (
      
         Klik på mig            );   } }
eksporter standard MyComponent;

Denne implementering bruger en klasseejendom i stedet for en tilstandsnøgle, fordi det clickedOnce-flag ikke repræsenterer en UI-tilstand, og derfor ikke bør bo inden i komponenttilstanden. Med denne nye implementering udløser ikke længere en opdatering ved at klikke på knappen mere end én gang.

Definer altid propTypes og defaultProps

Alle komponenter skal have propTypes og defaultProps defineret så højt som muligt inden for komponenten. De fungerer som komponentdokumentation og skal straks være synlige for andre udviklere, der læser filen.

Siden React v15.5 er React.PropTypes flyttet ind i en anden pakke, så lad os installere denne pakke som en afhængighed:

$ npm installation - gemme prop-typer

Til statsløse funktionelle komponenter:

Funktioner hejses i JavaScript, hvilket betyder, at du kan bruge en funktion inden dens erklæring:

import React fra 'react';
import PropTypes fra 'prop-typer';
MyComponent.propTypes = {
  titel: PropTypes.string,
};
MyComponent.defaultProps = {
  titel: 'En enkel tæller',
};
eksport standardfunktion MyComponent (rekvisitter) {
  returner 

{props.title}

; }

ESLint vil klage over at bruge funktionen inden dens definition, men med henblik på bedre komponentdokumentation, lad os deaktivere denne fnugningsregel for funktioner ved at ændre .eslintrc-filen:

{
  ...
  "regler": {
    "react / jsx-filnavn-udvidelse": 0,
    "ingen brug-før-definere": [
      "fejl",
      {
        "funktioner": falsk
      }
    ]
  }
}

For klassebaserede komponenter:

I modsætning til funktioner hejses ikke klasser i JavaScript, så vi kan ikke blot gøre MyComponent.propTypes = ... før vi definerer selve klassen, men vi kan definere propTypes og defaultProps som statiske klasseegenskaber:

import React, {Component} fra 'react';
import PropTypes fra 'prop-typer';
klasse MyComponent udvider komponent {
  statiske propTypes = {
    titel: PropTypes.string,
  };
  statisk defaultProps = {
    titel: 'En enkel tæller',
  };
  render () {
    returner 

{this.props.title}

;   } }
eksporter standard MyComponent;

Initialisering af staten

Tilstand kan initialiseres i komponentkonstruktøren:

klasse MyComponent udvider komponent {
  konstruktør (rekvisitter) {
    super (rekvisitter);
    this.state = {
      tæller: 0,
    };
  }
}

En bedre måde er at initialisere staten som en klasseejendom:

klasse MyComponent udvider komponent {
  konstruktør (rekvisitter) {
    super (rekvisitter);
  }
  tilstand = {
    tæller: 0,
  };
}

Dette ser meget bedre ud, renere, mere læsbar og bidrager også til komponentdokumentation. Tilstandsobjektet skal initialiseres efter propTypes og defaultProps:

import React, {Component} fra 'react';
import PropTypes fra 'prop-typer';
klasse MyComponent udvider komponent {
  // propTypes kommer først
  statiske propTypes = {
    titel: PropTypes.string,
  };
  // defaultProps kommer på andenplads
  statisk defaultProps = {
    titel: 'En enkel tæller',
  };
  // konstruktør kommer her
  konstruktør () {
    ...
  }
  // derefter kommer staten
  tilstand = {
    tæller: 0,
  };
}

Overfør en funktion til setState

React-dokumentation afskrækker at stole på this.state og this.props værdier til beregning af den næste tilstand, fordi React opdaterer dem asynkront. Det betyder, at staten måske ikke ændrer sig umiddelbart efter, at hun har ringet til setState ().

klasse MyComponent udvider komponent {
  tilstand = {
    tæller: 10,
  }
  onClick = () => {
    console.log (this.state.count); // 10
    
    // tælling ændres ikke med det samme
    this.setState ({count: this.state.count + this.props.step});
    
    console.log (this.state.count); // stadig 10
  }
}

Selvom dette fungerer til enkle scenarier, og staten stadig opdateres korrekt, kan det føre til uventet opførsel i mere komplekse scenarier.

Overvej dette scenario, du har en komponent, der gengiver en enkelt knap. Når du klikker på denne knap kaldes en handleClick-metode:

klasse MyComponent udvider komponent {
  statisk defaultProps = {
    trin: 5,
  }
  statiske propTypes = {
    trin: PropTypes.number,
  }
  
  tilstand = {
    tæller: 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 () {
    Vend tilbage (
      
        

Aktuelt antal er: {this.state.count}

         Klik på mig            );   } }

Knappen ringer til håndteringKlik (), når der klikkes, hvilket igen kalder doSomething () og derefter doSomethingElse (). Begge funktioner ændrer tællerværdien i staten.

Logisk set er 10 + 5 15 og trækkes derefter fra 1, og resultatet skal være 14, ikke? Nå, i dette tilfælde er det ikke - værdien af ​​tællingen efter det første klik er 9, ikke 14. Dette sker, fordi værdien af ​​this.state.count stadig er 10, når doSomethingElse () kaldes, ikke 15.

For at løse dette kan du bruge en anden form for setState (), der accepterer en funktion snarere end et objekt. Denne funktion modtager den forrige tilstand som det første argument, og rekvisitterne på det tidspunkt, hvor opdateringen anvendes som det andet argument:

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

Vi kan bruge denne formular til at rette vores eksempel:

klasse MyComponent udvider komponent {
  ...
  handleClick = () => {
    this.doSomething ();
    this.doSomethingElse ();
  }
  doSomething = () => {
    this.setState ((prevState, rekvisitter) => ({
      tæller: prevState.count + props.step
    }));
  }
  doSomethingElse = () => {
    this.setState (prevState => ({
      tæller: prevState.count - 1
    }));
  }
  ...
}

Med denne implementering opdateres antallet korrekt fra 10 til 14 til 18 osv. Simple Maths giver mening igen!

Brug pilefunktioner som klasseegenskaber

Dette søgeord har altid været forvirrende for JavaScript-udviklere, og dets opførsel er ikke mindre forvirrende i React-komponenter. Ved du, hvordan dette nøgleord ændrer sig i en React-komponent? Overvej følgende eksempel:

import React, {Component} fra 'react';
klasse MyComponent udvider komponent {
  tilstand = {
    tæller: 0,
  };
  onClick () {
    console.log (this.state);
  }
  render () {
    Vend tilbage (
      
        

Tælling er: {this.state.count}

         Klik på mig            );   } }
eksporter standard MyComponent;

Klik på knappen vil resultere i en fejl:

Ikke fanget TypeError: Kan ikke læse egenskaben 'tilstand' af udefineret

Dette skyldes, at onClick, som en klassemetode, ikke er bundet som standard. Der er nogle få måder at rette op på. (ordspil beregnet, får du det?)

En måde er at binde funktionen til den rigtige kontekst, når du passerer den inde i funktionen render ():

 Klik på mig 

Eller du kan undgå at ændre konteksten ved hjælp af en pilefunktion i render ():

 this.onClick (e)}> Klik på mig 

Imidlertid har disse to metoder en svag ydelsesomkostning, fordi funktionen vil blive tildelt på hver render. For at undgå denne lette ydelsesomkostning kan du binde funktionen inde i konstruktøren:

import React, {Component} fra 'react';
klasse MyComponent udvider komponent {
  konstruktør (rekvisitter) {
    super (rekvisitter);
    this.onClick = this.onClick.bind (dette);
  }
  ...
  render () {
    ...
    <-knap onClick = {this.onClick}> Klik på mig 
    ...
  }
}
eksporter standard MyComponent;

Denne teknik er bedre, men du kan let blive revet med og ende med noget der ser sådan ud:

konstruktør (rekvisitter) {
  // dette er dårligt, virkelig dårligt
  this.onClick = this.onClick.bind (dette);
  this.onChange = this.onChange.bind (dette);
  this.onSubmit = this.onSubmit.bind (dette);
  this.increaseCount = this.increaseCount.bind (dette);
  this.decreaseCount = this.decreaseCount.bind (dette);
  this.resetCount = this.resetCount.bind (dette);
  ...
}

Da vi bruger Babel og har support til klasseegenskaber, ville den bedre måde være at bruge pilefunktioner, når vi definerer klassemetoder:

klasse MyComponent udvider komponent {
  ...
  onClick = () => {
    // 'dette' er bevaret
    console.log (this.state);
  }
  render () {
    Vend tilbage (
      
        

{this.state.count}          Klik på mig            );   } }

Ødelæg Props-objektet

Når en komponent har mange rekvisitter, skal du ødelægge rekvisitaobjektet, der placerer hver egenskab på sin egen linje.

Til statsløse funktionelle komponenter:

eksport standardfunktion MyComponent ({
  fornavn,
  efternavn,
  email adresse,
  beskrivelse,
  onChange,
  onSubmit,
}) {
  Vend tilbage (
    
      

{firstName}       ...        ); }

Standardargumenter er ikke en undskyldning for at droppe defaultProps. Som nævnt tidligere skal du altid definere propTypes og defaultProps.

For klassebaserede komponenter:

klasse MyComponent udvider komponent {
  ...
  render () {
    const {
      fornavn,
      efternavn,
      email adresse,
      beskrivelse,
      onChange,
      onSubmit,
    } = this.props;
    Vend tilbage (
      
        

{firstName}         ...            );   } }

Dette er renere, gør det lettere at ombestille egenskaber og gør det lettere at tilføje / fjerne egenskaber til / fra listen, mens der genereres en læsbar diff for Git. Overvej følgende:

Afvigelse er mere læsbar, når hver egenskab er på en ny linje

På højre side kan du nemt fortælle, hvilken egenskab der blev tilføjet. På venstre side ved du kun, at noget har ændret sig på den linje, og du skal se virkelig omhyggeligt for at finde ud af, hvilken del af linjen der var ændret.

Betinget gengivelse

Når du skal gengive en af ​​to komponenter eller blokke af JSX-kode baseret på en betingelse, skal du bruge et ternært udtryk:

isLoggedIn
  ? 
Velkommen, {username}!
  : Login

Hvis koden består af mere end en linje, skal du bruge parenteser:

erLoggedIn? (
  
    Velkommen, {brugernavn}!    ): (   <-knap onClick = {this.login}>     Log på    )

Hvis du har brug for at gengive en enkelt komponent eller en blok af JSX-kode baseret på en betingelse, skal du bruge en kortslutningsevaluering i stedet:

isComplete && 
Du er færdig!

Brug flere parenteser i mere end en linje:

isComplete && (
  
    Du er færdig!    )

Nøgleattributten

Det er et almindeligt mønster at bruge Array.prototype.map og lignende array-metoder inde i render () -funktionen, og det er let at glemme nøgleattributten. Forsoning er hård nok, gør det ikke sværere. Husk altid at placere nøglen, hvor den hører til. (ordspil tiltenkt igen, får du det?)

render () {
  Vend tilbage (
    
    {       {         items.map (item => (           
  •             {tingens navn}                    ))        }        ); }

Når du bruger kort (), filter () eller lignende array-metoder, er den anden parameter for tilbagekald objektets indeks. Det er generelt en dårlig idé at bruge dette indeks som en nøgle. React bruger nøgleattributten til at identificere, hvilke poster der er ændret, er blevet tilføjet eller er blevet fjernet, og nøgleværdien skal være stabil og skal identificere hvert enkelt element unikt.

I tilfælde, hvor arrayet er sorteret, eller et element tilføjes til starten af ​​arrayet, ændres indekset, selvom elementet, der repræsenterer dette indeks, kan være det samme. Dette resulterer i unødvendige gengivelser og i nogle tilfælde resultater i visning af forkerte data.

Brug UUID eller ShortID til at generere en unik id for hvert element, når det først oprettes, og brug det som nøgleværdien.

Normaliser staten

Forsøg at normalisere tilstandsobjektet og hold det så fladt som muligt. Indlejringsdata i staten betyder, at der kræves en mere kompleks logik for at opdatere dem. Forestil dig, hvor grimt det ville være at opdatere et dybt indlejret felt - vent, forestil dig ikke, her:

// statsobjektet
tilstand = {
  ...
  indlæg: [
    ...,
    {
      meta: {
        id: 12,
        forfatter: '...',
        offentligt: ​​falsk,
        ...
      },
      ...
    },
    ...
  ],
  ...
};
// for at opdatere 'offentligt' til 'sandt'
this.setState ({
  ... this.state,
  indlæg: [
    ... this.state.posts.slice (0, indeks),
    {
      ... this.state.posts [indeks],
      meta: {
        ... this.state.posts [indeks] .meta,
        offentligt: ​​sandt,
      }
    },
    ... this.state.posts.slice (indeks + 1),
  ]
});

Denne kode er ikke kun grim, men den kan også tvinge ikke-relaterede komponenter til at gendanne, selvom de data, de viser, ikke er faktisk ændret. Dette skyldes, at vi har opdateret alle forfædre i statstræet med nye objekthenvisninger.

Det ville være enklere at opdatere det samme felt, hvis vi omstrukturerer statstræet:

tilstand = {
  indlæg: [
    10,
    11,
    12,
  ],
  meta: {
    10: {
      id: 10,
      forfatter: 'forfatter-a',
      offentligt: ​​falsk,
    },
    11: {
      id: 11,
      forfatter: 'forfatter-b',
      offentligt: ​​falsk,
    },
    12: {
      id: 12,
      forfatter: 'forfatter-c',
      offentligt: ​​falsk,
    },
  },
}
this.setState ({
  meta: {
    ... this.state.meta,
    [id]: {
      ... this.state.meta [id],
      offentligt: ​​sandt,
    }
  }
})

Brug klasserne

Hvis du nogensinde har været i en situation, hvor du har brug for at bruge et betinget klassnavn, har du måske skrevet noget, der ser sådan ud:

  ...

Dette bliver virkelig grimt, hvis du har flere betingede klassebetegnelser. I stedet for at bruge en ternary, skal du bruge klassens navnepakke:

import klasserne fra 'klasserne';
const Classes = classnames ('tab', {'is-active': isActive});
  ...

En komponent pr. Fil

Dette er ikke en hård regel, men jeg foretrækker at beholde hver komponent i en enkelt fil og standardeksport af den komponent. Jeg har ingen specifik grund til dette, men jeg synes det bare er mere rent og organiseret - high five, hvis du også gør det!

Konklusion

Der er ikke en rigtig måde at udvikle en React-komponent på. Vi har gennemgået et par af de bedste fremgangsmåder og mønstre, der vil hjælpe dig med at udvikle genanvendelige og vedligeholdelige komponenter, og jeg er meget nysgerrig efter at vide, hvilken slags mønstre og praksis du foretrækker, og hvorfor.

I den næste del af denne serie skal vi opbygge en simpel opgave-applikation og anvende nogle af disse bedste fremgangsmåder og mønstre.

Var denne artikel nyttig? Klik på knappen Klap nedenfor, eller følg mig for mere.

Tak for at have læst! Hvis du har nogen feedback, skal du skrive en kommentar nedenfor.

Gå til del 7-b: Opbygning af en simpel ToDo-app (snart)