React to biblioteka JavaScript, stworzona w 2013 roku i rozwijana przez Facebook. Fakt, że stoi za nią ogromna korporacja ma swoje wady i zalety, z jednej strony możemy być pewni, że pracują nad nią świetni programiści i wszystkie decyzje dotyczące jej rozwoju są podejmowane przez profesjonalistów, z drugiej jednak możemy mieć pewne opory moralne ze względu na specyficzne środki bezpieczeństwa związane z licencjonowaniem, o których niedawno było dość głośno.
Pomijając jednak sprawy polityczne, spróbujemy stworzyć od zera prostą aplikację wyświetlającą tabelkę z listą kryptowalut z Bittrex. Projekt w zasadzie nikomu do niczego nie potrzebny, ale jako pierwszy przykład z pewnością wystarczy.
Wykorzystujemy:
- React v15.6
- Express v4.15
- Babel v6.26
- Webpack v3.5
- Axios v0.16
- Bootstrap v3.3
Inicjalizacja projektu
Zaczynamy od klasycznego „npm init” i generujemy nowy plik package.json.
{
"name": "rekt",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Następnie pobieramy potrzebne biblioteki:
npm install --save-dev axios babel-cli babel-core babel-loader babel-preset-env babel-preset-react bootstrap react react-dom webpack npm install --save express
Może się wydawać dziwne dodawanie Bootstrapa do devDependiences, ale dla uproszczenia skopiujemy z niego tylko zawartość katalogu dist i nie będzie nam później potrzebny (opcjonalnie można wykorzystać CDN).
Do package.json dopisujemy linijki odpowiedzialne za skrypty i pluginy babel:
{
"name": "rekt",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "DEBUG=express:* node index.js",
"build": "webpack -p",
"watch": "webpack --progress --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"axios": "^0.16.2",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"bootstrap": "^3.3.7",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"webpack": "^3.5.6"
},
"dependencies": {
"express": "^4.15.4"
},
"babel": {
"presets": [
"env",
"react"
]
}
}
Tworzymy katalogi i kopiujemy Bootstrapa:
. ├── node_modules/ │ └── (bibliteki z npm) ├── package.json ├── public/ │ ├── css/ │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.min.css.map │ ├── fonts/ │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js/ │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── npm.js └── src/
Konfiguracja Webpack
W naszym przypadku, Webpack będzie potrzebny do przetłumaczenia przy pomocy Babel kodu ES6+JSX na klasyczny JavaScript rozumiany przez przeglądarki oraz do sklejenia wszystkich wymaganych plików w jeden bundle.js. Ostateczny webpack.config.js wygląda następująco:
var path = require('path');
require("babel-polyfill");
module.exports = {
entry: ['babel-polyfill', './src/main.js'],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'public')
},
module: {
loaders: [{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: "babel-loader"
}]
}
};
Czas na HTML
Cała logika aplikacji będzie realizowana przez JavaScript, dlatego też wystarczy nam minimalistyczny plik HTML. Tworzymy /public/index.html:
<!DOCTYPE html>
<html>
<head>
<title>REkT</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css"/>
</head>
<body>
<noscript>(╯°□°)╯︵ ┻━┻</noscript>
<div class="container-fluid">
<div class="row">
<div id="root"></div>
</div>
</div>
<script src="/bundle.js"></script>
</body>
</html>
Expressem do przodu
Kierując się zasadą strzelania z armaty do wróbli, framework Express posłuży wyłącznie do serwowania statycznych plików. Tworzymy index.js:
const express = require('express');
const app = express();
app.use(express.static('public'));
const listen = app.listen(process.env.PORT || 3000, () => {
console.log(listen.address());
});
Akcja, reakcja
W tym momencie jesteśmy już gotowi do napisania pierwszego kodu wykorzystującego bibliotekę React. Umieścimy go w pliku /src/main.js i po uruchomieniu Webpacka i Express zobaczymy wynik w przeglądarce.
Żeby za długo nie zwlekać i jednocześnie szybko przetestować czy wszystko działa zgodnie z założeniami, zaczniemy od klasycznego „Hello world”:
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return (
<p>Hello world</p>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Jak to w ogóle działa? Na początku importowane są biblioteki react oraz react-dom. Później deklarujemy klasę App wywodzącą się z React.Component, której jedynym zadaniem na tę chwilę jest udostępnienie metody render zwracającej kod HTML. Na koniec, wywołujemy metodę render z ReactDOM, zlecając jej podpięcie komponentu <App> (którego działanie jest zdefiniowane właśnie w klasie App) do elementu o identyfikatorze „root” (w naszym przypadku DIV w index.html).
Umieszczanie HTML bezpośrednio w kodzie JavaScript może się na pierwszy rzut oka wydawać dziwne, ale od tego właśnie mamy Babel, który przetłumaczy go na mniej więcej coś takiego:
var App = function (_React$Component) {
_inherits(App, _React$Component);
function App() {
_classCallCheck(this, App);
return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments));
}
_createClass(App, [{
key: 'render',
value: function render() {
return _react2.default.createElement(
'p',
null,
'Hello world'
);
}
}]);
return App;
}(_react2.default.Component);
_reactDom2.default.render(_react2.default.createElement(App, null), document.getElementById('root'));
Startujemy
Uruchamiamy dwa osobne terminale, przechodzimy w nich do folderu z naszym projektem i uruchamiamy w pierwszym z nich „npm run watch” a w drugim „npm run dev”.
Jeśli wszystko pójdzie z planem, w pierwszym uruchomi się Webpack i utworzy plik /public/bundle.js, po czym zacznie na bieżąco w pętli monitorować zmiany w plikach źródłowych.
Version: webpack 3.5.6
Time: 3250ms
Asset Size Chunks Chunk Names
bundle.js 1.05 MB 0 [emitted] [big] main
[140] (webpack)/buildin/global.js 509 bytes {0} [built]
[212] multi babel-polyfill ./src/main.js 40 bytes {0} [built]
[415] ./src/main.js 2.39 kB {0} [built]
+ 534 hidden modules
W drugim uruchomi się Express w trybie DEBUG i wyświetli informację o porcie na którym nasłuchuje serwer HTTP.
{ address: '::', family: 'IPv6', port: 3000 }
Pozostaje wpisać adres https://localhost:3000 do przeglądarki i zobaczyć efekt działania aplikacji. Dla pewności, w konsoli DevTools możemy zweryfikować czy paragraf z „Hello world” podpiął się przy pomocy React do DIV:
<html>
<head>
<title>REkT</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<noscript>(╯°□°)╯︵ ┻━┻</noscript>
<div class="container-fluid">
<div class="row">
<div id="root"><p data-reactroot="">Hello world</p></div>
</div>
</div>
<script src="/bundle.js"></script>
</body>
</html>
Gratulacje. Udało się nam wyświetlić dwa słowa przy pomocy pliku JS, który zajmuje aktualnie ponad 1MB w projekcie zawierającym ponad 60MB bibliotek.
I co dalej?
Zaczynamy od przebudowania /src/main.js:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
const url = 'https://bittrex.com/api/v1.1/public/getmarketsummaries';
ReactDOM.render(
<App url={url} />,
document.getElementById('root')
);
Po pierwsze, wyrzuciliśmy klasę App do zewnętrznego pliku /src/app.js (zaraz go utworzymy). Po drugie, do komponentu <App> przekazujemy parametr url zawierający adres API strony Bittrex, zwracający JSON z listą kryptowalut w formacie:
{
"success": true,
"message": "",
"result": [{
"MarketName": "BTC-1ST",
"High": 0.00008108,
"Low": 0.00007205,
"Volume": 1441030.45893439,
"Last": 0.00007391,
"BaseVolume": 110.40565565,
"TimeStamp": "2017-09-10T15:09:38.907",
"Bid": 0.00007359,
"Ask": 0.00007468,
"OpenBuyOrders": 143,
"OpenSellOrders": 5537,
"PrevDay": 0.00007645,
"Created": "2017-06-06T01:22:35.727"
}, {
"MarketName": "BTC-2GIVE",
"High": 0.00000143,
"Low": 0.00000115,
"Volume": 12007718.13364286,
"Last": 0.00000120,
"BaseVolume": 15.42232806,
"TimeStamp": "2017-09-10T15:08:28.83",
"Bid": 0.00000120,
"Ask": 0.00000121,
"OpenBuyOrders": 335,
"OpenSellOrders": 2249,
"PrevDay": 0.00000142,
"Created": "2016-05-16T06:44:15.287"
}, {
[...]
}]
}
Tworzymy klasę App w /src/app.js:
import React from 'react';
import axios from 'axios';
import CoinsTable from './coinstable';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
coins: []
};
}
componentDidMount() {
axios.get(this.props.url)
.then((res) => {
if (res && res.data && res.data.result && res.data.result.length) {
this.setState({
coins: res.data.result
});
}
});
}
render() {
return (
<div className="panel-body">
<CoinsTable coins={this.state.coins} />
</div>
);
}
}
export default App;
Tutaj zaczynają się dziać ciekawe rzeczy. Po pierwsze, w konstruktorze definiujemy this.state i wrzucamy do niego pustą tablicę o nazwie coins. Będziemy w niej później przechowywać dane pobrane z API Bittrex.
Metoda componentDidMount jest automatycznie wywoływana przez React po przypięciu komponentu <App> na stronie. W naszym przypadku, jej zadaniem jest wykonanie przy pomocy biblioteki Axios zapytania AJAX i wczytanie danych z adresu przekazanego do komponentu w parametrze url (jest dostępny w obiekcie this.props). Po przejściu zgrubnej weryfikacji, dane trafiają do this.state.coins.
Metoda render zwraca DIV zawierający kolejny komponent, tym razem CoinsTable, któremu przekazana zostaje tablica danych w parametrze coins. Należy zauważyć, że ze względu na fakt że „class” jest zastrzeżonym słowem w JavaScript, zmuszeni jesteśmy wpisywać w jego miejscu „className”.
Gwoli wyjaśnienia, w nomenklaturze React obiekt this.state zawiera podręczne dane komponentu w którym się znajdujemy i które możemy zmieniać przy pomocy metody this.setState. Obiekt this.props zawiera parametry przekazane przez komponent nadrzędny i jego elementy są tylko do odczytu.
Następny krok to klasa CoinsTable w /app/coinstable.js:
import React from 'react';
import CoinsRow from './coinsrow';
class CoinsTable extends React.Component {
render() {
if (this.props.coins && this.props.coins.length) {
const rows = [];
this.props.coins.forEach((coin) => {
rows.push(
<CoinsRow
key={coin.MarketName}
marketname={coin.MarketName}
high={coin.High}
low={coin.Low}
volume={coin.Volume}
/>
);
});
return (
<table className="table table-striped">
<thead>
<tr>
<th>MarketName</th>
<th>High</th>
<th>Low</th>
<th>Volume</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
} else {
return (
<p>loading...</p>
);
}
}
}
export default CoinsTable;
W tym komponencie nie potrzebujemy przechowywać stanu, bo jego działanie polega wyłącznie na wyświetleniu tego co otrzyma na liście parametrów w obiekcie this.props.
Metoda render na początku sprawdza, czy this.props.coins zawiera jakieś dane. Jeżeli tak jest, przygotowana jest nowa tablica o nazwie rows, składająca się z listy komponentów CoinsRow, do których są przekazane wartości poszczególnych komórek każdego z wierszy. Dodatkowo, każdy z nich otrzymuje parametr o nazwie „key”, zawierający unikatowy symbol umożliwiający bibliotece React szybką identyfikację elementów wymagających odświeżenia na wypadek zmian w danych. Na koniec zwracany jest kod gotowej tabeli w HTML.
W przypadku, gdy this.props.coins jest puste, otrzymamy tylko paragraf z treścią „loading…”.
Pozostała już tylko klasa CoinsRow w /app/coinsrow.js:
import React from 'react';
class CoinsRow extends React.Component {
render() {
return (
<tr>
<td>{this.props.marketname}</td>
<td>{this.props.high.toFixed(8)}</td>
<td>{this.props.low.toFixed(8)}</td>
<td>{this.props.volume.toFixed(3)}</td>
</tr>
);
}
}
export default CoinsRow;
Tutaj nie ma już nawet czego wyjaśniać, komponent zwraca pojedynczy wiersz tabeli z danymi w komórkach.
Gotowe!
Stworzyliśmy prostą aplikację, pobierającą dane z zewnętrznego API i wyświetlającą je w tabeli na stronie. W rzeczywistości nawet nie dotknęliśmy czubka możliwości biblioteki React, ale jak na pierwszy raz powinno to wystarczyć.

Kolejnym krokiem mogło by być dodanie możliwości przeszukiwania tabeli oraz sortowania danych w kolumnach. Przydała by się również opcja odświeżenia danych.
Uwagi
Metoda render w komponencie <App> jest wywoływana dwukrotnie. Za pierwszym razem z pustą tabelą this.state.coins, co skutkuje wyświetleniem napisu „loading”. Zaraz po tym zdarzeniu, React uruchamia metodę componentDidMount, która asynchronicznie pobiera dane z API. W chwili, gdy dane są gotowe, aplikacja modyfikuje this.state.coins przy pomocy this.setState a to automatycznie wymusza odświeżenie komponentu i ponowne uruchomienie metody render, tym razem już z gotową tabelą.
Ostateczna struktura projektu wygląda następująco:
. ├── index.js ├── node_modules/ │ └── (biblioteki z npm) ├── package.json ├── public/ │ ├── bundle.js (generowany przez webpack) │ ├── css/ │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.min.css.map │ ├── fonts/ │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── index.html │ └── js/ │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── npm.js ├── src/ │ ├── app.js │ ├── coinsrow.js │ ├── coinstable.js │ └── main.js └── webpack.config.js

Dodaj komentarz