Aby rozpocząć pracę z React musimy dobrze znać składnię ECMAScript 6 zwanej też ES6 czy ECMAScript 2015, która przynosi wiele rozwiązań dzięki którym JavaScript staje się nowoczesnym językiem. Co więcej w pracy z React nie musimy się przejmować starszymi przeglądarkami. Kompilator Babel przy generowaniu gotowego projektu automatycznie przetworzy kod, który będzie kompatybilny z starszymi przeglądarkami. Zaznaczam również że jest to ogólny wstęp do ES6, więc po więcej szczegółów trzeba dociekać we własnym zakresie, ale na start ten artykuł powinien być wystarczający 🙂
let i const
To dwa nowe słowa kluczowe w ES6 pozwalające na deklarację zmiennej oraz stałej. Dotychczas korzystaliśmy z var w JavaScript, ale wiązało się to z pewnymi problemami. Deklaracja zmiennej var ma zasięg funkcyjny a nie blokowy!
if (1 == 1) {
var someVarData = 100;
}
// var nie ma zakresu blokowego
// więc jest dostępna wartość
console.log(someVarData); // 100
function test(a) {
if (a == 1) {
// b ma zasięg funkcyjny, więc
// będzie dostęp tylko wewnątrz funkcji
var b = 7;
}
return b;
}
console.log(test(1)); // funkcja zwróci 7
// undefined bo b nie istnieje w globalnym window
// b ma zasięg funkcyjny, istnieje tylko wewnątrz
// funkcji test
console.log(window.b); // undefined
Natomiast let i const mają zakres blokowy czyli ich zakres ogranicza się do bloku kodu powstałego z ich umieszczenia w bloku if, pętli czy również funkcji, czyli tak jak w większości języków programowania. Dodatkowo stała czyli const po przypisaniu wartości nie może być już zmieniona, gdyż powstanie błąd:
if(1 == 1) {
let someLetData = 8;
}
// Uncaught ReferenceError: someLetData is not defined
// czyli istnieje tylko w bloku
console.log(someLetData);
Funkcja strzałkowa
Zwana również jako fat arrow jest jednym z ważniejszych dodatków w ES6. Po pierwsze taka funkcja jest krótsza w zapisie, bardziej czytelna, ale najważniejsze że ma zakres leksykalny. Otóż this używane w funkcji strzałkowej zachowuje kontekst swojego otoczenia co upraszcza odwoływanie się do obiektów na których pracujemy w JS.
// przed ES6 oprócz zwykłej funkcji można było zapisać też wyrażenie funkcyjne
const exprFoo = function (a, b) {
// funkcja zapisana do zmiennej
console.log(a, b);
};
// funkcja strzałkowa - arrow function, zapis bardziej skrócony
// => to tzw. fat arrow
const arrowBar = (a, b) => console.log(a, b);
// przy jednym parametrze oraz jednej instrukcji można pominąć nawiasy
const bar = (a) => console.log(a);
bar(true); // true
Funkcja strzałkowa świetnie nadaje się do przekazania jako callback np do funkcji forEach aby przejść po wszystkich elementach tablicy:
[2,4,8].forEach(function(a) {
console.log(a);
});
// bardziej czytelny zapis
[2,4,8].forEach( a => console.log(a) );
Zwracanie wartości przez funkcję strzałkową:
let test = (a,b) => { return a * b; };
// return można pominąć wraz z nawiasami
let multiply = (a,b) => a * b;
// => zwany fat arrow
const splitStr = str => str.split(" ");
var result = splitStr("jeden dwa trzy"); // ["jeden", "dwa", "trzy"]
// zwrócenie obiektu
let makeObj = () => { return {a:5, b: 10}; };
let arrowObj = () => ( {a:5, b:10} ); // pomijając return w nawiasach okrągłych
W przypadku użycia this wewnątrz funkcji strzałkowej mamy gwarancję, że jego wartość ma kontekst otoczenia napisanego kodu, czyli this jest leksykalne. Poniższy kod wywoła przekazaną funkcję strzałkową do setTimeout w kontekście obiektu na bazie Foo czyli this.data będzie miało wartość “test”. Gdyby została przekazana tradycyjna funkcja czy wyrażenie funkcyjne zamiast strzałkowej to setTimeout byłoby wywołane w kontekście obiektu window i this.data wtedy ma wartość 99.
window.data = 99; // problem z this rozwiązuje funkcja strzałkowa
function Foo() {
this.data = "test";
this.printInfo = function () {
setTimeout( () => { // this ma kontekst otoczenia
console.log("this.data:"+ this.data ); // pokaże test z Foo
}, 1000 );
}
}
const foo = new Foo();
foo.printInfo();// that.data:test
Funkcje strzałkowe znacząco upraszczają pisanie kodu, nie tylko jest bardziej czytelny, ale przede wszystkim posiadają leksykalne this, a nie zwykły kontekst wywołania.
Składnia rozwinięcia – spread syntax
Pozwala na rozbicie iterowanej wartości na składowe np elementy tablicy tabPart są płytko skopiowane do tablicy finalTab za pomocą trzech kropek:
let tabPart = [3, 4, 5];
let finalTab = [1, 2, ...tabPart, 6, 7]; // łączenie tablic
console.log(finalTab); // [1, 2, 3, 4, 5, 6, 7]
W React często będziemy dodawali nowy element do już istniejącej tablicy:
let tasksList = [
{ name: "Task 1", completed: false },
{ name: "Task 2", completed: true },
{ name: "Task 3", completed: true },
];
tasksList = [...tasksList, { name: "Task nr. 4", completed: false }];
Rest
Parametr rest czyli z angielskiego reszta pozwala na zebranie do tablicy pozostałych argumentów przekazanych do funkcji:
function test(a, ...args) {
console.log(a);
console.log(args);
}
// wyświetli:
// ok
// [2,3,4,5] zebrane do tablicy args
test("ok", 2, 3, 4, 5);
Argument “ok” został przekazany po staremu, ale dowolna ilość kolejnych argumentów została zapisana w tablicy args.
Destrukturyzacja
Pozwala w wygodny sposób na przypisanie właściwości tablic oraz obiektów do zmiennych.
let obj = { name: "Ola", city: "Wawa", age: 28 };
const {name, city, age} = obj; // dodanie oddzielnych stałych name, city i age
console.log(name, age); // Ola, 28
const tab = [1, "str", {a:5}];
const [num, text, data] = tab; // stałe na podstawie tablicy według kolejności jej danych
console.log(num, data); // 1, {a:5}
Można również destrukturyzować zagnieżdżony obiekt:
const user = {
id: 12,
name: "Rafał",
employment: {
company: "Example.com",
address: {
country: "Poland"
}
}
};
// utworzenie stałej company
const {employment: {company}} = user;
console.log(company); // Example.com
// utworzenie stałej country
const {employment: {address: {country}}} = user;
console.log(country); // Poland
// co jak chcemy miec inną nazwę stałej?
const {employment: {address: {country: companyCountry}}} = user;
console.log(companyCountry); // Poland
// domyślna wartość jeśli element street nie istnieje w address
const {employment: {address: {street = "Wilcza"} }} = user;
console.log(street); // Wilcza
Domyślne parametry funkcji
Jak sam tytuł wskazuje możemy podać domyślną wartość parametru funkcji:
function add(a, b = 5) {
return a + b;
}
add(3); // 8
add(1, 2); // 3
const sub = (a = 10, b = 6) => a - b;
sub(5); // -1
sub(5, 1); // 4
Zobaczmy również jak można łączyć destrukturyzację przekazanego obiektu do funkcji wraz z domyślnym parametrem:
// domyślny parametr z destrukturyzacją
function checkRank({username, rank = "guest"} = {username:"Guest"}) {
if (rank === "admin") {
console.log(username, "is admin, Welcome!");
} else {
console.log(username, "is", rank);
}
}
const user01 = { username: "Daniel", age: 35, rank: "admin"};
// przekazany obiekt jest destrukturyzowany bez użycia domyślnych wartości
checkRank(user01); // Daniel is admin, Welcome!
const otherUser = { username: "Daniel", age: 25};
// przekazany obiekt jest destrukturyzowany, ale nie posiada pola rank, więc
// destrukturyzacja zapewnia domyslną wartość guest
checkRank(otherUser); // Daniel is guest
// wywołanie funkcji bez parametru sprawia że dopiero teraz mamy
// użyty domyślny obiekt z username Guest
// dodatkowo też na nim jest destrukturyzacja! dzięki czemu mamy dostępne w funkcji
// username oraz rank (domyślna wartość z destrukturyzacji jako guest)
checkRank(); // Guest is guest
Z pewnością trzeba przećwiczyć tą składnię by się z nią oswoić 🙂
Klasy
Wreszcie mamy dostęp do klas w JavaScript ale pamiętajmy że jest to wyłącznie cukier składniowy, gdyż pod spodem nadal wszystko jest oparte na prototypach. Mimo wszystko i tak jest to spore ułatwienie 🙂
class Car {
// konstruktor wywoła się gdy instancja obiektu
// będzie utworzona. Można pominąć konstruktor
// jesli nie jest nam potrzebny, wtedy js wywoła
// domyślny pusty konstruktor
constructor() {
this.brand = "Dodge";
}
model = "Viper" // tak też możemy dodać pole
// pole statyczne można użyć bez powołania instancji klasy
static numWheels = 4
printInfo() {
console.log(this.brand, this.model);
}
getNumWheels = () => {
console.log("Num wheels:", Car.numWheels);
}
};
const car1 = new Car();
car1.printInfo(); // "Dodge Viper"
Dodatkowo możemy naszą klasę rozszerzyć dzięki extends oraz wywołać nadrzędny konstruktor dzięki super()
class Van extends Car {
constructor(vanName, topSpeed = 170) {
// wywołanie konstruktora Car()
super();
// przesłaniamy nazwę modelu z Car
this.model = vanName;
this.topSpeed = topSpeed;
}
static getBestVan = () => {
return new Van("Grand Caravan", 215);
}
}
const van = new Van("Caravan");
van.printInfo(); // Dodge Caravan
const van2 = Van.getBestVan();
van2.printInfo(); // Dodge Grand Caravan
Moduły
ES6 ułatwia dzielenie aplikacji JavaScript na moduły, które rezydują w oddzielnych plikach zachowując swoją prywatność. Jeżeli czegoś nie udostępnimy z pliku za pomocą słowa kluczowego export to będzie traktowane jako prywatna część kodu wyłącznie dla danego pliku.
Poszczególne funkcje, klasy itd możemy importować za pomocą instrukcji import. Tak zdefiniowane moduły nie zanieczyszczają globalnej przestrzeni nazw.
Moduły można tylko importować do modułu, nie można tego zrobić w zwykłym skrypcie.
Moduły zawsze działają w trybie ścisłym (strict).
Można wyeksportować funkcję, obiekt, prymitywne wartości, a następnie dzięki instrukcji import można je zaimportować do programu. Mamy dwa rodzaje eksportów: nazwane i domyślne. W ponższym przykładzie są np. nazwane eksporty dla info, foo czy bar.
Można je zaimportować za jednym razem do obiektu przestrzeni nazw modułu dzięki “import * as”
<!-- strona1.html -->
<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<script type="module" src="es6module1.js"></script>
<script type="module">
import * as myModule from "./es6module1.js";
myModule.foo(); // foo from module1: 99
// Uncaught ReferenceError: info is not defined
console.log(info); // info prywatną zmienną
console.log(myModule.info); // undefined
</script>
</body>
</html>
Poniżej plik es6module1.js dla powyższego pliku strona1.html:
// plik es6module1.js
let info = "es6module.js";
// named export, funkcja publiczna
export function foo() {
console.log("foo from module1: "+data);
}
export function bar() {
foo();
}
let data = 99;
export default data;// domyślny export
Oprócz eksportu nazwanego istnieje również eksport domyślny, ale uwaga można zrobić tylko jeden taki eksport dla modułu. W przykładzie poniżej jest jeden domyślny eksport wartości zmiennej data z pliku js, następnie jest zaimportowana do someData. Zauważmy że tutaj nie ma znaczenia nazwa zmiennej, może być np someData, gdyż to domyślny import:
<!-- strona2.html -->
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<script type="module" src="es6module2.js"></script>
<script type="module">
import * as myModule from "./es6module2.js";
// import domyślnego eksportu czyli wartości 99
import someData from "./es6module2.js";
myModule.foo(); // foo from module2: 99
console.log(someData); // 99
</script>
</body>
</html>
I związany z powyższym plikiem html moduł es6module2.js
// plik es6module2.js
let info = "es6module2.js";
// named export, funkcja publiczna
export function foo() {
console.log("foo from module2: "+data);
}
export function bar() {
foo();
}
let data = 99;
export default data;// domyślny export
W React moduły przydadzą się nam przede wszystkim do tworzenia komponentów np klasa będzie komponentem i domyślnym eksportem z pliku, następnie zaimportujemy ją do naszego głównego pliku i osadzimy w html. Temat modułów jest obszerny, dlatego zawsze pamiętajmy o niezastąpionej dokumentacji MDN.
Template literals
Literały szablonu pozwalają nie tylko na pisanie tekstu w wielu wierszach, ale również na osadzenie wartości, tworzymy go za pomocą backtików `
const num = 99;
const text = `Text with number: ${num}
also it's multiline text!
also js expression: ${2*2}`;
console.log(text);
/* rezultat:
Text with number: 99
also it's multiline text!
also js expression: 4 */
Promises
Obietnica to specjalny obiekt reprezentujący operację wykonywaną w tle, która w przyszłości ma być zakończona. Funkcja getRates() zwraca obietnice do której przekazywana jest właściwa funkcja, która wykona asynchroniczny kod. Jeśli się powiedzie zostanie wywołany callback resolve, jeśli błąd reject. Obietnice sprawiają, że kod asynchroniczny jest bardziej czytelny.
function getRates(url) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open("GET", url);
req.onload = function () {
if (req.status == 200) {
resolve(req.response); // obietnica spełniona
} else {
reject(req.statusText); // Odrzucona obietnica
}
};
req.onerror = function () {
reject(Error("Network error."));
};
req.send();
});
}
Funkcja getRates() zwraca nową obietnicę w której to zdefiniowana funkcja jest wywołana i w zależności od rezultatu obietnica będzie spełniona bądź nie. Oba przypadki możemy wygodnie obsłużyć dzięki udostępnionym procedurom .then() oraz .catch()
const url = "http://api.nbp.pl/api/exchangerates/tables/a/last/?format=json";
getRates(url)
.then((response) => console.log(response))
.catch((err) => console.log(err));
Wywołując then() na obietnicy przekazujemy funkcję, która będzie uruchomiona jeśli kod wykona się bezbłędnie wewnątrz obietnicy. Natomiast gdy będzie błąd to użyta zostanie funkcja przekazana do .catch(). W praktyce jeśli serwer odpowie prawidłowo uzyskamy ostatnią tabelę kursów walut w formacie json w konsoli.
Zamiast funkcji getRates możemy wykorzystać gotową funkcję fetch do obsługi komunikacji z serwerem. Funkcja text() odpowiedzi z serwera da nam reprezentację tekstową danych. Funkcja json() zwróci również promise dlatego ponownie wykorzystujemy then() aby odczytać sparsowane dane z serwera.
fetch(url)
.then((response) => response.json())
.then((data) => console.log(data))
.catch((err) => console.log(err));
async / await
To nowy sposób pisania asynchronicznego kodu w ES6. Gdy dowolna funkcja zostanie poprzedzona słowem kluczowym async to stanie się funkcją asynchroniczną, która zwróci automatycznie znaną nam obietnicę.
async function test() {
return "Hello";
}
test().then((res) => {
console.log(res);
});
// zwróci Hello
Wywołanie funkcji test razem z async zwraca obietnicę. Następnie użycie then da nam dostęp do wyniku funkcji test().
Skoro async zwraca obietnicę to słowo kluczowe await sprawia że JavaScript poczeka na wykonanie kodu asynchronicznego. Uwaga: await można używać tylko wewnątrz funkcji poprzedzonej słowem async:
function loadData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data loaded");
}, 1000);
});
}
async function startApp() {
const data = await loadData(); // await poczeka na zakończenie asynchronicznej funkcji
console.log(data);
const rates = await fetch(
"http://api.nbp.pl/api/exchangerates/tables/a/last/"
);
console.log(rates);
}
startApp(); // uzyskamy tabelę kursów walut po około sekundzie
Podsumowanie
To dość obszerny artykuł a i tak nie mamy opisanych wszystkich nowości w ES6 jak: generatory, symbole, Map, Set itd. Natomiast na nasze potrzeby pracy z Reactem powinno być to wystarczające. Czas aby wreszcie przejść do pracy z React! Zapraszam do kolejnego artykułu 🙂