Utveckling & Arkitektur

Skrivet 2017-09-06

Detta är del 4 i bloggen Kickstarta din Front-end med React. I del 1 går vi igenom installation av Node.js, Create React App och Yarn. Vi går också igenom hur du startar appen. Om du inte känner dig bekväm med att intallera detta själv så kan du läsa del 1 här: Kickstarta din Front-end med React - Del 1: Förberedelser.

I del 2 av bloggen går vi igenom JSX och React. Hur Babel översätter JSX till anrop av React.createElement och fokuserar på dess parametrar: component, props, och ...children. Du kan läsa del 2 här: Kickstarta din Front-end med React - Del 2: React och JSX.

I del 3 av bloggen går vi igenom hur vi kan skapa egna komponenter och skicka data till dom med props. Du kan läsa del 3 här: Kickstarta din Front-end med React - Del 3: Komponenter och props

 

Jag utgår från att du skapat ett projekt med Create React App och öppnat projektet i din texteditor.

Följande versioner av programvara används:

node@6.11.0
npm@3.10.10
yarn@0.24.6
create-react-app@1.3.1

 

src/App.js

Öppna App.js som vi i del 3 redigerat, där finner du följande:

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {
render() {
return (
<div className="App">
<AppHeader
welcome="Agero"
date={new Date(Date.now()).toLocaleString()}
/>
<AppIntro />
</div>
);
}
}

const AppHeader = props =>
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to {props.welcome},the current date is {props.date}</h2>
</div>;

const AppIntro = () =>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>;

export default App;

Tre komponenter, eller en komponent som innehåller två andra komponenter. I förra delen av bloggen fokuserade vi på AppHeader och AppIntro. Nu ska vi istället fokusera på App, och vad som skiljer den från de andra två komponenterna. 

AppIntro är den simplaste versionen av en komponent. Det är en funktion som alltid returnerar samma sak, det är en konstant. Hur vi än anropar den så kommer den returnera samma sak. Om vi nu tittar på AppHeader istället, så vet vi att den tar emot ett argument: props. Med hjälp av props kan vi ta emot data från komponenter ovanför att reagera på det. Vi får alltså en mer dynamisk komponent. Om vi nu tar och kollar på App så ser vi ganska snabbt att inte är en funktion, som AppHeader och AppIntro. Man brukar separera React-komponenter i två olika implementationer: Functional (Stateless) Component och Class Component. Hitills har vi använt oss av de två första varianterna, och vi ska gå in mer på detta.

 

Functional Stateless Component

Både AppHeader och AppIntro är exempel på detta. En funktion som tar ett eller inget argument. Om funktionen tar ett argument så är detta props. En funktion som inte tar något argument kan ses som en konstant. Vi tar AppIntro som exempel:

const AppIntro = () =>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>;

Denna funktion tar inget argument och returnerar alltid samma sak. Alltså är det en konstant. Vi jämför med AppHeader:

const AppHeader = props =>
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to {props.welcome},the current date is {props.date}</h2>
</div>;

Denna funktion tar ett argument, och returnvärdet av funktionen beror på detta argument. Det viktiga att inse här är att det är argumentet som påverkar returvärdet. Ingen yttre faktor kan påverka vad funktionen returnerar. Det kan liknas vid en matematisk funktion ƒ(x):

ƒ(x) = 7 eller ƒ(x) = 3x

Dessa funktioner "beter" sig på samma sätt. Den första funktionen är en konstant och den andra funktionen beror bara på dess argument x.

Om vi nu tar och tittar på App, och framför allt dess metod render så ser vi att upplägget inte skiljer sig så mycket från våra två andra exempel. Det som skiljer sig är att komponenter är definierad som en instans av en klass som ärver från React.Component. En sådan komponent kallas Class Component.

 

Class Component

Det som skiljer en Class Component från en Functional Stateless Component är state. state, är som det låter, state... Ett värde definierat av användaren som kan ändras över tid. Detta för att ha ett värde som används i render-metoden som kommer ifrån komponenten själv, inte nedskickat som props från en komponent ovanför. Vad en Class Component renderar beror alltsp på props och state, medans en Functional Stateless Component endast beror på props. Komponenten har också ett flertal API:er för att uppdatera detta state vid olika tillfällen. Vi kommer använda dessa API:et för att få vår komponent uppdatera tiden, så att den tickar som en klocka.

 

state

För att introducera state i vår komponent gör vi följande:

class App extends Component {
state = {
date: new Date(Date.now())
}

render() {
return (
<div className="App">
<AppHeader
welcome="Agero"
date={this.state.date.toLocaleString()}
/>
<AppIntro />
</div>
);
}
}

state är ett JavaScript-objekt. För att referera till det i vår render-metod så skriver vi this.state. Vi initialiserar vårt state till dagens datum, och skickar ned det som props till AppHeader, webbsidan bör se ut precis som tidigare. Vi har ändrat implementationsdetaljer av App, men så länge vi skickar ned samma data till AppHeader så fungerar allt som det ska.

Vi har dock fortfarande samma problem som i tidigare blogginlägg, datumet uppdateras inte! Vi tänker oss att vi löser detta genom att implementera en funktion som anropas varje sekund, med rätt datum. Vi börjar med att implementera själva funktionen som uppdaterar datumet:

class App extends Component {
state = {
date: new Date(Date.now())
}

tick() {
this.state.date = new Date();
}

render() {
return (
<div className="App">
<AppHeader
welcome="Agero"
date={this.state.date.toLocaleString()}
/>
<AppIntro />
</div>
);
}
}

Öppna din terminal där du kört yarn start och du bör finna detta:

Compiled with warnings.

./src/App.js
Line 11: Do not mutate state directly. Use setState() react/no-direct-mutation-state

Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

Den varnar alltså för följande: Do not mutate state directly. Use setState(). Detta innebär att vi inte får mutera (ändra) vår state variabel direkt med en ny tilldelning, som vi nu precis gjort på rad 11. Vi ska använda oss av en funktion setState. Vi ska gå in mer på setState.

 

setState

setState är en funktion som i dess enklaste form tar ett argument, ett objekt. Detta objekt motsvarar ditt state , eller en del av det, och dess nya värde. Vi kommer alltså skriva om

this.state.date = new Date();

till

this.setState({
  date: new Date()
});

så vårt argument är ett object med en date-property där vi tilldelar det nya värdet. state ska anses immutable och enda sättat att ändra detta värde är att använda sig av setState.

Det är också värt att veta att setState merge:ar ditt gamla state med ditt nya. Vad detta innebär i praktiken är att om vi har ytterligare ett värde på vårt state likt detta:

state = {
date: new Date(Date.now()),
color: 'Red'
}

och anropar detta:

this.setState({
  date: new Date()
});

så kommer color att förbli 'red', det är alltså ändast property:n vi anger i setState som uppdateras, inget annat.

Vi har nu lärt oss hur vi ska uppdatera vårt datum, men vart ska vi göra det? Vi vet att vi vill uppdatera värdet varje sekund, så vi behöver någon form av timer. Vi kan enkelt göra detta med följande:

this.timer = setInterval(() => this.tick, 1000);

Vi kommer nu anropa vår tick-funktion varje sekund. Notera ES6-syntaxen:

() => this.tick

Detta är alltså en annonym funktion som returnerar this.tick. setInterval tar en funktion eller uttryck som första argument, detta är alltså vår funktion.

Men vart ska vi placera denna kod? Vi kommer lösa detta med så kallade Lifecycle methods. Lifecycle methods är den andra delen som skiljer en Class Component ifrån en Functional Stateless Component!

 

Lifecycle

Lifecycle Methods är metoder som körs vid olika delar av en komponents livscykel. En komponent har olika stadier i dess livscykel, och vi kan exekvera saker vid dessa stadier. Det första stadiet vi ska introducera kallas mounting, detta är skedet då komponented renderas till webbsidan DOM för första gången. Motsatsen till detta är unmounting, det är när komponenten tas bort från webbsidans DOM.

Vi kan skriva kod som exekveras vid detta två tillfällen på detta sätt:

class App extends Component {
state = {
date: new Date(Date.now())
};

componentDidMount() {}

componentWillUnmount() {}

tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div className="App">
<AppHeader welcome="Agero" date={this.state.date.toLocaleString()} />
<AppIntro />
</div>
);
}
}

Det passar ju då perfekt att starta vår timer när komponenten renderas för första gången, och att sedan ta bort timern när komponenten försvinner. Vi lägger till timer-funktionaliteten:

componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timer);
}

Om du öppnar upp din webbsida så bör klockan nu ticka! Vi ska gå igenom vad vi gjort och varför.

 

Sammanfattning

Vi började med att introducera state i vår Class Component (App)

state är ett användardefinierat värde kopplat till komponentet som kan ändras under komponentens livscykel. Två exempel på detta livscykel är när komponenten renderas i webbsidans DOM och när den tas bort från webbsidan som. Dessa två livscykel-metoder är componentDidMount och componentWillUnmount. Med componentDidMount ser vi till att starta vår klocka när komponenten renderas på webbsidan, och inte innan! Med hjälp av componentWillUnmont ser vi också till att sluta räkna när komponenten försvinner på webbsidan.

Det stora som skiljer en Class Component från en Functional Stateless Component är just detta. En Functional Stateless Component beter sig mer som en matematisk funktion ƒ(x) = 3x.  

Det är viktigt att komma ihåg att state endast får ändra med setState-funktionen. setState tar emot ett objekt som argument och merge:ar detta objekt med ditt tidigare state.

 

Nedanför är resultatet av vår kod, med lite små ändringar som jag kommentera och markerat ljusgrönt.

Vi applikation bör se ut så här efter alla ändringar:

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {
state = {
// datumed uppdateras av tick-funktionen när komponenten monteras
// behöver inte hämta datumet direkt här
date: ""
};

// när komponenten renderas så börjar vi räkna
componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}

// när komponenten tas bort slutar vi räkna
componentWillUnmount() {
clearInterval(this.timer);
}

// hämta nytt datum
tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div className="App">
// skicka bara ned datumet som props, låt AppHeader sköta formatteringen
<AppHeader welcome="Agero" date={this.state.date} />
<AppIntro />
</div>
);
}
}

// lägg till paranteser kring uttrycket, blir lite lättare att läsa
const AppHeader = props => (
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>
// formattera props (date)
Welcome to {props.welcome}, the current date is {props.date.toLocaleString()}
</h2>
</div>
);

// lägg till paranteser kring uttrycket, blir lite lättare att läsa
const AppIntro = () => (
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
);

export default App;

 

Här är koden utan kommenterna:

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {
state = {
date: ""
};

componentDidMount() {
this.timer = setInterval(() => this.tick(), 1000);
}

componentWillUnmount() {
clearInterval(this.timer);
}

tick() {
this.setState({
date: new Date()
});
}

render() {
return (
<div className="App">
<AppHeader welcome="Agero" date={this.state.date} />
<AppIntro />
</div>
);
}
}

const AppHeader = props => (
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>
Welcome to {props.welcome}, the current date is {props.date.toLocaleString()}
</h2>
</div>
);

const AppIntro = () => (
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
);

export default App;

 

Är du sugen på att bli en av oss? Läs om vår företagskultur eller lämna in en spontanansökan.

Läs om vår företagskulturJag vill också bli kollega