Book list
Prvi korak biće pravljenje komponente sa imenom BookList koja će predstavljati "wapper" komponenti koje će predstavljati svaku knjigu pojedinačno.
Krećemo tako što pravimo novi fajl (svaka komponenta = novi fajl) unutar foldera components
. Unutar njega napisaćemo neki osnovni kod za početak samo da prikažemo neki tekst. Nešto tipa
import React from 'react';
import { View, Text } from 'react-native';
const BookList = () => {
return (
<View>
<Text> Book List!!</Text>
</View>
)
};
export default BookList;
Prikazaćemo novu komponentu ispod Header-a tako da prvo je importujemo (unutar index fajla).
import BookList from './src/components/BookList';
Da bi je prikazali ubacujemo je ispod <Header /> tag-a, neophodna stvar kada koristimo JSX jeste da svaka komponenta koju napravimo mora da vraća jednu "top level" JSX tag. Što znači da ne možemo samo napisati
const App = () => {
return (
<Header headerText='Books' />
<BookList />
);
};
Jer se na ovaj način vraćaju dva elementa istog nivoa, pa se u ovakvim slučajevima oni okružuju View
elementom i na taj način zadovoljava potreba JSX-a za top level tagom. Vaš index fajl treba ovako da izgleda:
// Import-ovati odgovarajuce module
import React from 'react';
import { AppRegistry, View } from 'react-native';
import Header from './src/components/Header';
import BookList from './src/components/BookList';
// Kreairati komponentu
const App = () => {
return (
<View>
<Header headerText='Books' />
<BookList />
</View>
);
};
// Prikazati komponentu (Render it)
AppRegistry.registerComponent('books', () => App);
Ukoliko odradite reload vašeg simulatora ispod Header komponente videćete tekst koji ste napisali unutar BookList komponente.
HTTP request
Sledeći korak će nam biti dovođenje potrebne data-e sa našeg endpointa, i to ćemo učiniti pomoću HTTP requesta unutar React-a.
Do ovog trenutka koristili smo samo funkcionalne komponente, što znači da nam je svaka komponenta bila funkcija koja je vraćala odredjeni JSX koji se prikazuju na ekranu. I jedini uslov za ovakve komponente jeste da vraća neki JSX kod.
Da bi napravili HTTP request moraćemo koristit drugi tip komponenti a to su Class Based Components. O razlikama možete pogledati ovde.
Ali u kratkim crtama, Functional Components su korisne kada se prikazuje statičke informacije (data), i jako su jednostavne za pisanje (kao što smo do sada mogli da vidimo), međutim sa njima ne postoji način handle-ovanja rada sa dinamičkom data-om, dok se Class Components (nazivaju se Class based jer se zasnimavaju na ES6 classes) koriste upravo za to, rad sa dinamičkim informacijama, informacijama koje se mogu menjati vremenom, zatim korisničkim event-ovima i sl. I nije kompikovanija za pisanje ali je potrebno malo više koda.
Tako da ćemo refaktorisati našu BookList komponentu, tako da je iz funkcionalne prebacimo u Class Based komponentu.
Komponenta sada izgleda ovako :
import React, { Component } from 'react';
import { View, Text } from 'react-native';
class BookList extends Component {
render() {
return (
<View>
<Text> Book List!!</Text>
</View>
);
}
}
export default BookList;
Potrebno je importovati Component koji je obezbedjen od strane React-a, njega extends-uje naša klasa. Kad god napišemo Class Based komponentu moramo definisati funkciju render()
unutar nje, koja vraća odredjeni JSX koji će se prikazivati.
Sada je vreme da fetch-ujemo data-u sa API. Prvo pitanje je kada treba da se fetch-uje data, sada stupa na scenu nešto što nam Class Based components omogućavaju a to su lifecycle metode.
Mi ćemo koristiti metodu componentWillMount(), koja će se izvršiti neposredno pre render-ovanja naše komponete. Koristimo nju, jer nam je data potrebna pre nego što nam se prikaže komponenta kako bi mogli da na osnovu te dobijene data-e izrenderujemo komponente koje će predstavljati knjige.
Čim ubacimo ovu metodu, ona će se automatski pozivati svaki put pre render-ovanja naše komponente.
componentWillMount() {
console.log('ComponentWillMount is called');
}
Da bi ovo proverili unutar komponente ćemo staviti jedan console.log . Da bi videli konzolu kliknite na simulator i priskom na cmd + D dobićete meni gde treba izabrati "Debug JS remotely". Tada vam se otvara novi tab u browser-u i desnim klikom na njega i kliknom na "inspect" (kao i na veb aplikacijama) možete videti konzolu u kojoj će se prikazivati vaši console.log-ovi. Ovo vam preporučujem da koristite radi lakšeg debug-ovanja i praćenja rada vaše aplikacije.
Da bi fetch-ovali našu data-u, koristićemo biblioteku "axios". Prvo ćemo je instalirati pomoću NPM-a.
npm install --save axios
Zatim importovati i iskoristiti unutar metode ComponentWillMount()
componentWillMount() {
axios.get('https://private-f364e-booksapi11.apiary-mock.com/books')
.then(response => console.log(response));
}
URL korišćen ovde je napravljen na apiary-u, u vreme vašeg čitanja pitanje je da li će biti aktivan, tako da predlažem da sami napravite svoj endpoint.
Kada otvorite konzolu u browser-u i reload-ujete vaš simulator, treba da vidite response Promise-a napravljenog dobijenog od axios-ovog get-a. Unutar dobijenog objekta imamo listu "data" koja sadrži nama potrebnu data-u.
Sledeći problem koji treba da rešimo jeste taj što je HTTP request odvija asihrono, što znači da se data može vratiti za par milisekundi, ali to može da traje i dosta duže(pogotovu na mobilnim uredjajima).
Nakon što se request pošalje, naša komponenta se izrenderuje(prazna, bez date), ali na neki način joj trebamo reći da se izrenderuje ponovo kada data stigne. Tu u pomoć dolazi Component State.
Prvi korak je da unutar naše klase, inicijalizujemo default, početno stanje našeg state-a
class BookList extends Component {
state = { books: [] };
....
Na internetu možete pronaći više načina za inicijalizaciju state-a, to se može učiniti u konstruktoru klase, ili metodom getInitialState. Na vama je da odlučite šta ćete da koristite.
Ovim smo jednostavno rekli, da se sa klasom inicijalizuje i njen state koji u sebi sadrži prazan niz sa imenom books. Samim tim, klasa će dobiti property kome može pristupiti sa this.state
. Sledeći korak je menjanje state-a unutar metode coponentWillMount() koja se izvršava neposredno pre render-a komponente.
componentWillMount() {
axios.get('https://private-f364e-booksapi11.apiary-mock.com/books')
.then(response => this.setState({ books: response.data }));
}
Ovim korakom postižemo to da se nakon što fetch-ujemo data-u, odmah je smestimo u naš Component State (da bi mogli da je koristimo u komponenti kroz this.state.books
). Iz gore navedene slike možete videti da iz response-a nam je potreban samo data
array, zato njega pomoću funkcije this.setState
smeštamo gde želimo. Bitno je napomenuti da se samo pomoću ove funkcije update-uje state komponente i da se state koristi samo unutar Class Based komponenti, kao i da se pozivanjem ove funkcije izvršava ponovno renderovanje komponente, što je nama i potrebno. Tako da to rešava naš problem, nakon inicijalizacije komponente ona će se izrenderovati, a zatim kada odgovor sa data-om stigne, ona će to učiniti ponovo i moći će da iskoristi dospelu data-u.
Prikažimo samo šta dobijamo unutar state-a. Unutar render
metode napišimo:
console.log(this.state);
Kada reload-ujete svoj app i pogledate konzolu videćete sledeće
što vam govori da se metoda render
izvršila dva puta, pri prvom render-u books niz je bio prazan, a zatim kada je data stigla i smestila se u state komponente, render se ponovo izvršio i možete videti da je niz books sada pun.
Jedan podsetnik, kada želite da ostvarite "komunikaciju" izmedju dve komponente (prosledite neku data-u), parent-a i child-a za to ćete koristiti props-e, dok je state za interno, unutar jedne komponente, "beleženje" data-e, i njeno korišćenje unutar te iste komponente.
Sada je vreme da dobijenu data-u iskoristimo.
Želimo da napravimo komponentu unutar koje će se nalaziti lista komponeti. I svaka ta unutrašnja komponenta će prikazivati jedan objekat iz niza books koji se nalazi u state-u komponente.
Prvo ćemo napisati jednu metodu
renderBooks() {
return this.state.books.map(book => <Text>{ book.title }</Text>);
}
koja uzima niz books i "mapira" kroz njega. Funkcija map vraća novi niz sa istim brojem članova, i pritom prolazi kroz svaki član starog niza i izvršava datu funkciju. Tako da u ovom slučaju ova map funkcija vraća novi niz unutar čega će biti <Text> tagovi unutar kojih će se nalazite nazivi knjiga. Da se podsetimo, kada hoćemo da napravimo neku javascript referencu unutar jsx-a, korisitmo vitičaste zagrade.
Sada ispravimo malo i render metodu komponente
render() {
return (
<View>
{ this.renderBooks() }
</View>
);
}
Kada reload-ujemo naš simulator videćemo listu naziva svih knjiga koje smo dobili kao data-u.
Na dnu simulatora, možete primetiti warning vezan za "key" prop, problem je u tome što react zahteva da kada postoji niz istih komponenti (renderBooks vraća niz <Text> komponenti), svaka sadrži jedinstveni key property, ovo se radi samo radi boljih performansi pri ponovno renderovanju. Ovo ćemo zadovoljiti ovako:
renderBooks() {
return this.state.books.map((book, key) => <Text key={ key }>{ book.title }</Text>);
}
Cela BookList
komponenta izleda ovako
import React, { Component } from 'react';
import { ScrollView } from 'react-native';
import axios from 'axios';
class BookList extends Component {
state = { books: [] };
componentWillMount() {
axios.get('https://private-f364e-booksapi11.apiary-mock.com/books')
.then(response => this.setState({ books: response.data }));
}
renderBooks() {
return this.state.books.map((book, key) => <Text key={ key }>{ book.title }</Text>);
}
render() {
return (
<ScrollView>
{ this.renderBooks() }
</ScrollView>
);
}
}
export default BookList;
Book Detail Component
Sada ćemo napraviti novu komponentu BookDetail
, koja će primati jedan item iz liste books, i koristiti te informacije unutar sebe.
Krećemo tako što prvo pravimo novi fajl unutar components direktorijuma (BookDetail.js - svaka komponenta poseban fajl). Unutar fajla ispisaćemo osnovni kod
import React from 'react';
import { View, Text } from 'react-native';
const BookDetail = () => {
};
export default BookDetail;
Sada, pre nego što počnete da pišete komponentu, treba razmisliti da li nam je potrebna Class Based component ili Functional component.
U ovom slučaju znamo da će ova komponenta samo prikazivati neku data-u, i da nam neće trebati niti state, niti lifecycle metode, tako da se odlučujemo za funkcionalnu komponentu.
Sada možemo importovati ovu komponentu untar BookList (sada modifikujemo BookList.js)
import BookDetail from './BookDetail';
I zatim je iskoristimo unutar renderBooks() metode
renderBooks() {
return this.state.books.map((book, key) =>
<BookDetail key={ key } book={ book } />
);
}
Ovde smo jednostavno, umesto <Text> taga iskoristili <BookDetail> komponentu, i pomoću props-a book
prosledili (od strane parent-a, child komponenti) data-u book, tako da će se sada tim informacijama unutar komponente BookDetail
pristupti sa this.props.book
.
Napomenimo da ime props-a može biti šta god želite, samo je predlog da imena budu dovoljno jasna.
Pristupimo sada props-ima prosledjenim BookDetail komponent unutar nje:
const BookDetail = (props) => {
return (
<View>
<Text>{ props.book.title }</Text>
</View>
);
};
I sada, kada odradite reload simulatora, nikakve promene nećete videti, ali svaki od naslova predstavlja zasebnu BookDetail
komponentu koju sada možemo modifikovati kako želimo.
Ideja je da BookDetail
komponente izgleda kao kartica na kojoj će se prikazivati ime knjige i ime autora, zatim slika, i na kraju dugme koje će voditi na amazon.com gde se ta knjiga može kupiti. Tako da bi učinili komponente što više "reusable" , struktura će izgledati ovako :
- BookDetail sadrži komponentu Card
- Card komponenta sadrži komponente CardSection
- Unutar komponenti CardSection će se nalaziti ili tekst, ili slika ili button.
Ovakvu podelu radimo, samo da bi imali veću kontrolu nad stilovima ovih komponenti, jer bi bilo izuzetno teže kada vi sve stavljali i "nestovali" unutar jedne komponente.
Card component
Klasičan redosled, prvo pravimo fajl Card.js unutar components direktorijuma i zatim pisemo standardan početni kod
import React from 'react';
import { View, Text } from 'react-native';
const Card = () => {
return (
<View></View>
);
};
export default Card;
Razlog postojanja ove komponente jeste da "samo bude lepa" i da obuhvati ostale komponente unutar nje. Zato krenimo odmah sa stilizovanjem iste.
const styles = {
cardStyle: {
borderWidth: 1,
borderRadius: 2,
borderColor: '#ddd',
borderBottomWidth: 0,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 1,
marginLeft: 5,
marginRight: 5,
marginTop: 10
},
};
Mislim da nema potrebe previše objašnjavati ovaj styles objekat, princip je isti kao i pre, iz naziva property-ja je poprilično jasno šta rade, i još samo treba reći komponenti da koristi ovaj stil, a i to vam je poznato
const Card = () => {
const { cardStyle } = styles;
return (
<View style={ cardStyle }></View>
);
};
Sada iskoristimo ovu komponentu unutar BookDetail komponente tako što ćemo je prvo importovati
import Card from './Card';
A zatim zameniti je sa <View> tagom, jer želimo da nam ona bude "wrapper".
const BookDetail = (props) => {
return (
<Card>
<Text>{ props.book.title }</Text>
</Card>
);
};
Sada jedna bitna informacija. Pored props-a, koje parent komponenta može da prosledi child komponenti, mogu se proslediti i druge komponente, i to na ovaj gore ispisan način. Izmedju <Card> tagova nalazi se <Text> tag, ili ti komponenta. Svemu što se nalazi izmedju <Card> tagova, unutar Card komponente moguće je pristupiti kroz props.children
. Tako da malim izmenama unutar Card komponente možemo prikazati sve to unutar nje
const Card = (props) => {
const { cardStyle } = styles;
return (
<View style={ cardStyle }>
{ props.children }
</View>
);
};
Sada kada odradite reload simulatora, primetićete razdvojene kartice sa imenima knjiga
CardSection component
CardSection je jako slična Card komponenti, napravljena iz sličnih razloga (da bi se lakše stilizovala) i prikazuje sve što se nalazi izmedju njenih tagova.
import React from 'react';
import { View } from 'react-native';
const CardSection = (props) => {
const { cardSectionStyle } = styles;
return (
<View style={ cardSectionStyle }>
{ props.children }
</View>
);
};
const styles = {
cardSectionStyle: {
borderBottomWidth: 1,
padding: 5,
backgroundColor: '#fff',
justifyContent: 'flex-start',
flexDirection: 'row',
borderColor: '#ddd',
position: 'relative'
}
};
export default CardSection;
Iskoristimo je unutar CardDetail komponente, prvo importovanje
import CardSection './CardSection';
i zatim je postavimo oko <Text> taga
const BookDetail = (props) => {
return (
<Card>
<CardSection>
<Text>{ props.book.title }</Text>
</CardSection>
</Card>
);
};
Kada odradite reload simulatora, videćete blage izmene unutar Card komponenti.