Reducers & Actions

Selection Reducer

Sada ćemo se pozabaviti određivanjem otvorene tehnologije. Podsetimo se da aplikacija treba da sadrži listu tehnologija pri čemu kada se klikne na jednu od njih ona se "otvori" i prikaže određeni tekst koji predstavlja njeno objašnjene. Kako bi aplikacija bila "svesna" koji item je otvoren, napravićemo novi reducer za to. Unutar folder reducers pravimo fajl SelectionReducer.js i unutar njega pisemo

export default () => {
  return null;
};

I zatim unutar index.js -a unutar istog foldera pišemo

import { combineReducers } from 'redux';
import TechnologyReducer from './TechnologyReducer';
import SelectionReducer from './SelectionReducer';

export default combineReducers({
  libraries: TechnologyReducer,
  selectedTechnology: SelectionReducer
});

Sada selectedTechonology će nam biti dostupan unutar App state-a. Ideja je selectedTechnology predstavlja otvoreni item tako što će sadržati broj koji će biti jednak id property-ju item-a koji je definisan u našem .json fajlu. Tako da na primer ako je otvoren Webpack item, selectedTechnology će imati vrednost 0.

[
  {
    "id": 0,
    "title": "Webpack",
    "description": "Webpack is a module bundler. It packs CommonJs/AMD modules i. e. for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand."
  },
  ....

Mi smo postvili da pri prvom render-ovanj selectedTechnologybude nullšto znači da ni jedna tehnologija nije otvorena.

Action creators

selectedTechnology se nalazi u app state-u. Da bi menjali njenu vrednost potrebno da je menjamo app state, a to rade reducer-i. Reducer-i se pokreću pozivanjem akcija, a za pozivanje akcija pravimo Action creators-e. (I to je sve, NISTA STRAŠNO :) ).

Prvo ćemo unutar root foldera napraviti novi folder actions . Unutar njega pravimo novi fajl index.js . Naša aplikacije će imati samo jedan action creator, jer nam je potreban samo za menjanje selectedTechnology . U nekim većim aplikacijama ovde bi smeštali sve naše akcije. index.js

export const selectTechnology = (technologyId) => {
  return {
    type: 'select_technology',
    payload: technologyId
  };
};

Podsetimo se da je akcija "plain" javascript objekat koji mora imati type property. Ovaj type property se može posmatrati kao instrikcija za reducer-e.

Akcija je obuhvaćena funkcijom koju nazivamo "Action creator". Svaki put kada pozovemo ovaj action creator, objekat ili ti akcija sa type property-jem select_technology će biti poslata svim reducer-ima. I na osnovu type -a se određuje koji reducer se pokreće. Pored type prosleđujemo i payload koji u sebi sadrži technologyId to jest vrednost koju treba dobiti slectedTechnology unutar app state-a, a taj id će biti prosleđen pri pozivu action creator-a.

Povezivanje Action creators-a i komponenti

Pošto će se ova akcija pozivati kada se klikne na određenu tehnologiju (item) unutar liste, prvo ćemo je import-ovati unutar komponente ListItem

import * as actions from '../actions';

Na ovaj način smo import-ovali sve akcije iz /actions foldera i imamo pristup njima preko actions objekta.
Sledeći korak je importovati connect iz react-redux, jer on predstavlja konekciju između React-a i Redux-a.

import { connect } from 'react-redux';

Pre smo ga koristili kako bi pristupili app state-u, ali sada ćemo ga koristi kako bi pozvali Action Creator.

export default connect(null, actions)(ListItem);

Kako nemamo nikakvu map-u unutar ove komponente connect-u kao prvi parametar prosleđujemo null , dok drugi parametar predstavljaju akcije koje smo import-ovali, i na ovaj način će one biti dostupne unutar komponente kroz props.
Ukoliko ubacite console.log(this.props) unutar render() metode komponente i reload-ujete svoj simulator, u konzoli ćete videti sledeće

Što znači da nam je akcija selectTechnology dostupna unutar komponente.

Pozivanje Action Creator-a

Sada kada je akcija dostupna unutar komponente, treba omogućiti da ona bude pozvana unutar komponente. Za to ćemo koristiti jednu od "touchable" komponenti. One su već komentarisane kada smo pravili Button komponentu, i kada smo koristili TouchableOpacity komponentu koja je na klik davala određeni odgovor korisniku (kada kliknete na dugme menja se opacity i korisnik je siguran da je dugme pritisnuto). U ovom slučaju nama nije potrebno da komponenta vraća bilo kakav "feedback" korisniku, jer u trenutku kada se klikne na ime tehnologije, ona će se "otvoriti" i prikazati tekst. Zato ćemo koristiti TouchableWithoutFeedback komponentu, koja u potpunosti odgovara našim potrebama.
Krećemo prvo sa import-ovanje ove komponente i View komponenten

import { Text, TouchableWithoutFeedback, View } from 'react-native';

Zatim ćemo wrap-ovati celu komponentu

class ListItem extends Component {
  render() {
    const { data, selectTechnology } = this.props;
    const { titleStyle } = styles;

    return (
      <TouchableWithoutFeedback onPress={() => selectTechnology(data.id)}>
        <View>
          <CardSection>
            <Text style={ titleStyle }>
              { data.title }
            </Text>
          </CardSection>
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

I na ovaj način svakim klikom na bilo koju od komponenti poziva se selectTechnology() akcija sa parametrom koji predstavlja id tehnologije koju komponenta predstavlja. Svaki put kada se "opali" neka akcija ona se šalje svim reducer-ima i zatim se na osnovu type property-ja određeni reducer pokreće.
Menjamo naš SelectionReducer

export default (state = null, action) => {
  switch (action.type) {
    case 'select_technology':
      return action.payload;
    default:
      return state;
  }
};

Unutar njega proveravamo type property, i ukoliko je njegova vrednost select_technology on će vratiti payload (tj vrednost id) koja je stigla iz akcije, i postavili smo default da vraća prethodnu vrednost state-a. Pošto se pri prvom renderovanju svaki reducer izvrši moramo staviti početno vrednost state-a da ne bude undefined jer će to izazvati grešku, i zato stavljamo da ukoliko state nije definisan on uzima vrednost null ( export default (state = null, action) ovakav način definisanja omogućava ES6).

Sada je potrebno da ovu vrednost dovedemo do naše komponente i proverima da li njena vrednost jednaka vrednošću id te komponente i ako jeste da tu komponentu "otvorimo" to jest da prikažemo tekst o tehnologiji koju ta komponente predstavlja.
To ćemo uraditi na sada već poznat način. Unutar ListItem komponente iskoristićemo connect funkciju

const mapStateToProps = state => {
  return {
    selectedTechnologyId: state.selectedTechnologyId
  };
};

export default connect(mapStateToProps, actions)(ListItem);

Dodajemo mapStateToProps i prosleđujemo ga kao prvi parametar connect funkciji. I sada ovom property-ju možemo pristupati kroz this.state.selectedTechnology.

Sada ćemo napisati jednu pomoćnu funkciju

renderDescription() {
  const { data, selectedTechnologyId} = this.props;
  if (data.id === selectedTechnologyId) {
    return (
      <Text>{ data.description }</Text>
    );
  }
}

Unutar nje proveravamo da li je id tehnologije jednak selectedTechnologyId i ako jeste vraćamo Text elemente unutar koga se prikazuje opis tehnologije.

I postavićemo je ispod prve CardSection komponente (mesto gde će se treba prikazati tekst)

render() {
  const { data, selectTechnology } = this.props;
  const { titleStyle } = styles;

  return (
    <TouchableWithoutFeedback onPress={() => selectTechnology(data.id)}>
      <View>
        <CardSection>
          <Text style={ titleStyle }>
            { data.title }
          </Text>
        </CardSection>
        { this.renderDescription() }
      </View>
    </TouchableWithoutFeedback>
  );
}

Ukoliko sada reload-ujete simulator i kliknete na neku od tehnologija, videćete i njen opis.

Još jedna od dobrih praksa je da se logika unutar komponenti snižava na najmanji mogući nivo. Zato našu proveru unutar pomoćne funkcije renderDescription() if (data.id === selectedTechnologyId) možemo odraditi unutar mapStateToProps i to na sledeći način

const mapStateToProps = (state, ownProps) => {
  const expended = state.selectedTechnologyId === ownProps.data.id;
  return { expended };
};

mapStateToProps pored state parametra ima i ownProps parametar koji sadrži isto što i this.props unutar komponente. Tako da se može odraditi provera (ista kao unutar if)

const expended = state.selectedTechnologyId === ownProps.data.id;

I to funkcija vraća, tako da je sada property expended dostupan kroz this.props.expended. Možemo i izmeniti pomoćnu funkciju

renderDescription() {
  const { data, expended} = this.props;
  if (expended) {
    return (
      <Text>{ data.description }</Text>
    );
  }
}

Možete proveriti u svom simulatoru, komponente bi treblo da se ponašaju identično.

U našem slučaju, ovo prebacivanje logike van komponenti ne poboljšava skoro ništa, ali treba imati to u vidu i držati se toga prilikom razvoja većih aplikacija sa mnogo više logike i komponenti.

Zarad stilizovanja dela sa opisom dodaćemo novi objekat unutar styles objekta

const styles = {
  titleStyle: {
    fontSize: 18,
    paddingLeft: 15
  },
  descStyle: {
    paddingLeft: 15,
    paddingRight: 15,
  }
};

Dodali smo samo padding sa obe strane kako bismo odvoji text od ivica. Dodelićemo ga Text elementu u našoj pomoćnoj funkciji i pri tome okružiti je CardSection komponentom.

renderDescription() {
  const { data, expended} = this.props;
  const { descStyle } = styles;

  if (expended) {
    return (
      <CardSection>
        <Text style={ descStyle }>{ data.description }</Text>
      </CardSection>
    );
  }
}

I poslednja stvar koju ćemo dodati jeste animacija otvaranja delova sa opisom tehnologija. To ćemo uraditi koristeći LayoutAnimation koji obezbeđuje react-native. Prvo ga import-ujemo

import { Text, TouchableWithoutFeedback, View, LayoutAnimation } from 'react-native';

I jedino sto je potrebno je da ga iskoristimo unutar lifecycle funkcije

componentWillUpdate() {
    LayoutAnimation.spring();
}

Na ovaj način se obaveštava funkcija da se animira bilo kakva promena unutar nje. Pošto je nama jedina promena otvaranje i zatvaranje komponenti ovo nam savršeno odgovara. Ukoliko sada reload-ujete svoj simulator i počnete da pritiskate po tehnologijama, videćete animirano otvaranje i zatvaranje komponenti.

ListItem.js treba ovako da izgleda

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Text, TouchableWithoutFeedback, View, LayoutAnimation } from 'react-native';
import CardSection from './CardSection';
import * as actions from '../actions';

class ListItem extends Component {
  componentWillUpdate() {
    LayoutAnimation.spring();
  }

  renderDescription() {
    const { data, expended} = this.props;
    const { descStyle } = styles;

    if (expended) {
      return (
        <CardSection>
          <Text style={ descStyle }>{ data.description }</Text>
        </CardSection>
      );
    }
  }

  render() {
    const { data, selectTechnology } = this.props;
    const { titleStyle } = styles;

    return (
      <TouchableWithoutFeedback onPress={() => selectTechnology(data.id)}>
        <View>
          <CardSection>
            <Text style={ titleStyle }>
              { data.title }
            </Text>
          </CardSection>
          { this.renderDescription() }
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

const styles = {
  titleStyle: {
    fontSize: 18,
    paddingLeft: 15
  },
  descStyle: {
    paddingLeft: 15,
    paddingRight: 15,
  }
};

const mapStateToProps = (state, ownProps) => {
  const expended = state.selectedTechnologyId === ownProps.data.id;
  return { expended };
};

export default connect(mapStateToProps, actions)(ListItem);

Ovime smo završili i našu poslednju komponentu.

Srećno u daljem radu! :)

results matching ""

    No results matching ""