Jatketaan muistiinpanosovelluksen yksinkertaistetun redux-version laajentamista.

Sovelluskehitystä helpottaaksemme laajennetaan reduceria siten, että storelle määritellään alkutila, jossa on pari muistiinpanoa:

const initialState = [
  {
    content: 'reducer defines how redux store works',
    important: true,
    id: 1,
  },
  {
    content: 'state of store can contain any data',
    important: false,
    id: 2,
  },
]

const noteReducer = (state = initialState, action) => {
  // ...
}

// ...
export default noteReducer

Monimutkaisempi tila storessa

Toteutetaan sovellukseen näytettävien muistiinpanojen filtteröinti, jonka avulla näytettäviä muistiinpanoja voidaan rajata. Filtterin toteutus tapahtuu radiobuttoneiden avulla:

fullstack content

Aloitetaan todella suoraviivaisella toteutuksella:

import React from 'react'
import NewNote from './components/NewNote'
import Notes from './components/Notes'

const App = (props) => {
  const store = props.store

  const filterSelected = (value) => () => {    console.log(value)  }
  return (
    <div>
      <NewNote store={store}/>
      <div>
        <div>
          all          <input type="radio" name="filter"            onChange={filterSelected('ALL')} />          important    <input type="radio" name="filter"            onChange={filterSelected('IMPORTANT')} />          nonimportant <input type="radio" name="filter"            onChange={filterSelected('NONIMPORTANT')} />        </div>
      </div>
      
      <Notes store={store} />
    </div>
  )
}

Koska painikkeiden attribuutin name arvo on kaikilla sama, muodostavat ne nappiryhmän, joista ainoastaan yksi voi olla kerrallaan valittuna.

Napeille on määritelty muutoksenkäsittelijä, joka tällä hetkellä ainoastaan tulostaa painettua nappia vastaavan merkkijonon konsoliin.

Päätämme toteuttaa filtteröinnin siten, että talletamme muistiinpanojen lisäksi sovelluksen storeen myös filtterin arvon. Eli muutoksen jälkeen storessa olevan tilan tulisi näyttää seuraavalta:

{
  notes: [
    { content: 'reducer defines how redux store works', important: true, id: 1},
    { content: 'state of store can contain any data', important: false, id: 2}
  ],
  filter: 'IMPORTANT'
}

Tällä hetkellähän tilassa on ainoastaan muistiinpanot sisältävä taulukko. Uudessa ratkaisussa tilalla on siis kaksi avainta, notes jonka arvona muistiinpanot ovat sekä filter, jonka arvona on merkkijono joka kertoo mitkä muistiinpanoista tulisi näyttää ruudulla.

Yhdistetyt reducerit

Voisimme periaatteessa muokata jo olemassaolevaa reduceria ottamaan huomioon muuttuneen tilanteen. Parempi ratkaisu on kuitenkin määritellä tässä tilanteessa uusi, filtterin arvosta huolehtiva reduceri:

const filterReducer = (state = 'ALL', action) => {
  switch (action.type) {
    case 'SET_FILTER':
      return action.filter
    default:
      return state
  }
}

Filtterin arvon asettavat actionit ovat siis muotoa

{
  type: 'SET_FILTER',
  filter: 'IMPORTANT'
}

Määritellään samalla myös sopiva action creator -funktio. Sijoitetaan koodi moduuliin src/reducers/filterReducer.js:

const filterReducer = (state = 'ALL', action) => {
  // ...
}

export const filterChange = filter => {
  return {
    type: 'SET_FILTER',
    filter,
  }
}

export default filterReducer

Saamme nyt muodostettua varsinaisen reducerin yhdistämällä kaksi olemassaolevaa reduceria funktion combineReducers avulla.

Määritellään yhdistetty reduceri tiedostossa index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'import App from './App'
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'
const reducer = combineReducers({  notes: noteReducer,  filter: filterReducer})
const store = createStore(reducer)
console.log(store.getState())

ReactDOM.render(
  <div></div>,  document.getElementById('root')
)

Koska sovelluksemme hajoaa tässä vaiheessa täysin, komponentin App sijasta renderöidään tyhjä div-elementti.

Konsoliin tulostuu storen tila:

fullstack content

eli store on juuri siinä muodossa missä haluammekin sen olevan!

Tarkastellaan vielä yhdistetyn reducerin luomista

const reducer = combineReducers({
  notes: noteReducer,
  filter: filterReducer,
})

Näin tehdyn reducerin määrittelemän storen tila on olio, jossa on kaksi kenttää, notes ja filter. Tilan kentän notes arvon määrittelee noteReducer, jonka ei tarvitse välittää mitään tilan muista kentistä. Vastaavasti filter kentän käsittely tapahtuu filterReducer:in avulla.

Ennen muun koodin muutoksia, kokeillaan vielä konsolista, miten actionit muuttavat yhdistetyn reducerin muodostamaa staten tilaa. Lisätään seuraavat tiedostoon index.js:

import { createNote } from './reducers/noteReducer'
import { filterChange } from './reducers/filterReducer'
//...
store.subscribe(() => console.log(store.getState()))
store.dispatch(filterChange('IMPORTANT'))
store.dispatch(createNote('combineReducers forms one reduces from many simple reducers'))

Kun simuloimme näin filtterin tilan muutosta ja muistiinpanon luomista Konsoliin tulostuu storen tila jokaisen muutoksen jälkeen:

fullstack content

Jo tässä vaiheessa kannattaa laittaa mieleen eräs tärkeä detalji. Jos lisäämme molempien reducerien alkuun konsoliin tulostuksen:

const filterReducer = (state = 'ALL', action) => {
  console.log('ACTION: ', action)
  // ...
}

Näyttää konsolin perusteella siltä, että jokainen action kahdentuu:

fullstack content

Onko koodissa bugi? Ei. Yhdistetty reducer toimii siten, että jokainen action käsitellään kaikissa yhdistetyn reducerin osissa. Usein tietystä actionista on kiinnostunut vain yksi reduceri, on kuitenkin tilanteita, joissa useampi reduceri muuttaa hallitsemaansa staten tilaa jonkin actionin seurauksena.

Filtteröinnin viimeistely

Viimeistellään nyt sovellus käyttämään yhdistettyä reduceria, eli palautetaan tiedostossa index.js suoritettava renderöinti muotoon

ReactDOM.render(
  <App store={store} />,
  document.getElementById('root')
)

Korjataan sitten bugi, joka johtuu siitä, että koodi olettaa storen tilan olevan mustiinpanot tallettava taulukko:

fullstack content

Korjaus on helppo. Viitteen store.getState() sijaan kaikki muistiinpanot sisältävään taulukkoon viitataan store.getState().notes:

const Notes = ({ store }) => {
  return(
    <ul>
      {store.getState().notes.map(note =>        <Note
          key={note.id}
          note={note}
          onClick={() => store.dispatch(toggleImportanceOf(note.id))}
        />
      )}
    </ul>
  )
}

Eriytetään näkyvyyden säätelyfiltteri omaksi, tiedostoon sijoitettavaksi src/components/VisibilityFilter.js komponentiksi:

import React from 'react'
import { filterChange } from '../reducers/filterReducer'

const VisibilityFilter = (props) => {

  const filterClicked = (value) => {
    props.store.dispatch(filterChange(value))
  }

  return (
    <div>
      all    
      <input 
        type="radio" 
        name="filter" 
        onChange={() => filterClicked('ALL')}
      />
      important   
      <input
        type="radio"
        name="filter"
        onChange={() => filterClicked('IMPORTANT')}
      />
      nonimportant 
      <input
        type="radio"
        name="filter"
        onChange={() => filterClicked('NONIMPORTANT')}
      />
    </div>
  )
}

export default VisibilityFilter

Toteutus on suoraviivainen, radiobuttonin klikkaaminen muuttaa storen kentän filter tilaa.

Komponentti App yksinkertaisuu nyt seuraavasti:

import React from 'react'
import Notes from './components/Notes'
import NewNote from './components/NewNote'
import VisibilityFilter from './components/VisibilityFilter'

const App = (props) => {
  const store = props.store

  return (
    <div>
      <NewNote store={store} />
      <VisibilityFilter store={store} />
      <Notes store={store} />
    </div>
  )
}

export default App

Muutetaan vielä komponentin Notes ottamaan huomioon filtteri

const Notes = ({ store }) => {
  const { notes, filter } = store.getState()  const notesToShow = () => {    if ( filter === 'ALL' ) {      return notes    }    return filter === 'IMPORTANT'      ? notes.filter(note => note.important)      : notes.filter(note => !note.important)  }
  return(
    <ul>
      {notesToShow().map(note =>        <Note
          key={note.id}
          note={note}
          onClick={() => store.dispatch(toggleImportanceOf(note.id))}
        />
      )}
    </ul>
  )
}

Huomaa miten storen tilan kentät on otettu tuttuun tapaan destrukturoimalla apumuuttujiin

const { notes, filter } = store.getState()

siis on sama kuin kirjoittaisimme

const notes = store.getState().notes
const filter = store.getState().filter

Sovelluksen tämänhetkinen koodi on githubissa branchissa part6-2.

Sovelluksessa on vielä pieni kauneusvirhe, vaikka oletusarvosesti filtterin arvo on ALL, eli näytetään kaikki muistiinpanot, ei vastaava radiobutton ole valittuna. Ongelma on luonnollisestikin mahdollista korjata, mutta koska kyseessä on ikävä, mutta harmiton feature, jätämme korjauksen myöhemmäksi.

Connect

Reduxin käytön ansiosta sovelluksen rakenne alkaa jo olla mukavan modulaarinen. Pystymme kuitenkin vielä parempaan.

Eräs tämänhetkisen ratkaisun ikävistä puolista on se, että Redux-store täytyy välittää propseina kaikille sitä tarvitseville komponenteille. App ei itse tarvitse ollenkaan Reduxia, mutta joutuu silti välittämään sen eteenpäin lapsikomponenteille:

const App = (props) => {
  const store = props.store

  return (
    <div>
      <NewNote store={store}/>  
      <VisibilityFilter store={store} />    
      <Notes store={store} />
    </div>
  )
}

Otetaan nyt käyttöön React Redux -kirjaston määrittelemä funktio connect, joka on tämän hetken defacto-ratkaisu sille, miten Redux-store saadaan välitettyä React-componenteille.

Connect voi olla aluksi haastava sisäistää, mutta hieman vaivaa kannattaa ehdottomasti nähdä. Tutustutaan nyt connectin käyttöön.

npm install --save react-redux

Edellytyksenä kirjaston tarjoaman connect-funktion käytölle on se, että sovellus on määritelty React redux -kirjaston tarjoaman Provider-komponentin lapsena ja että sovelluksen käyttämä store on annettu Provider-komponentin attribuutiksi store.

Eli tiedosto index.js tulee muuttaa seuraavaan muotoon

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'import App from './App'
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'

const reducer = combineReducers({
  notes: noteReducer,
  filter: filterReducer
})

const store = createStore(reducer)

ReactDOM.render(
  <Provider store={store}>    <App />  </Provider>,  document.getElementById('root')
)

Tutkitaan ensin komponenttia Notes. Funktiota connect käyttämällä "normaaleista" React-komponenteista saadaan muodostettua komponentteja, joiden propseihin on "mäpätty" eli yhdistetty haluttuja osia storen määrittelemästä tilasta.

Muodostetaan ensin komponentista Notes connectin avulla yhdistetty komponentti:

import React from 'react'
import { connect } from 'react-redux'import Note from './Note'
import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = ({ store }) => {
  // ...
}

const ConnectedNotes = connect()(Notes)export default ConnectedNotes

Moduuli eksporttaa nyt alkuperäisen komponentin sijaan yhdistetyn komponentin, joka toimii toistaiseksi täsmälleen alkuperäisen komponentin kaltaisesti.

Komponentti tarvitsee storesta sekä muistiinpanojen listan, että filtterin arvon. Funktion connect ensimmäisenä parametrina voidaan määritellä funktio mapStateToProps, joka liittää joitakin storen tilan perusteella määriteltyjä asioita connectilla muodostetun yhdistetyn komponentin propseiksi.

Jos määritellään:

const Notes = (props) => {
  // ...
}

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const ConnectedNotes = connect(mapStateToProps)(Notes)

export default ConnectedNotes

on komponentin Notes sisällä mahdollista viitata storen tilaan, esim. muistiinpanoihin suoraan propsin kautta props.notes sen sijaan, että käytettäisiin suoraan propseina saatua storea muodossa props.store.getState().notes. Vastaavasti props.filter viittaa storessa olevaan filter-kentän tilaan.

Komponentti muuttuu seuraavasti

const Notes = (props) => {  const notesToShow = () => {
    if ( props.filter === 'ALL' ) {      return props.notes    }

    return props.filter === 'IMPORTANT'      ? props.notes.filter(note => note.important)      : props.notes.filter(note => !note.important)  }

  return(
    <ul>
      {notesToShow().map(note =>
        <Note
          key={note.id}
          note={note}
          onClick={() =>
            props.store.dispatch(toggleImportanceOf(note.id))
          }
        />
      )}
    </ul>
  )
}

Connect-komennolla ja mapStateToProps-määrittelyllä aikaan saatua tilannetta voidaan visualisoida seuraavasti:

fullstack content

eli komponentin Notes sisältä on propsien props.notes ja props.filter kautta "suora pääsy" tarkastelemaan Redux storen sisällä olevaa tilaa.

Notes viittaa edelleen propsien avulla saamaansa funktioon dispatch, jota se käyttää muuttamaan Reduxin tilaa:

<Note
  key={note.id}
  note={note}
  onClick={() =>
    props.store.dispatch(toggleImportanceOf(note.id))  }
/>

Propsia store ei kuitenkaan ole enää olemassa, joten tilan muutos ei tällä hetkellä toimi.

Connect-funktion toisena parametrina voidaan määritellä mapDispatchToProps eli joukko action creator -funktioita, jotka välitetään yhdistetylle komponentille propseina. Laajennetaan connectausta seuraavasti

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const mapDispatchToProps = {  toggleImportanceOf,}
const ConnectedNotes = connect(
  mapStateToProps,
  mapDispatchToProps)(Notes)

Nyt komponentti voi dispatchata suoraan action creatorin toggleImportanceOf määrittelemän actionin kutsumalla propsien kautta saamaansa funktiota koodissa:

<Note
  key={note.id}
  note={note}
  onClick={() => props.toggleImportanceOf(note.id)}/>

Eli se sijaan että kutsuttaisiin

props.store.dispatch(toggleImportanceOf(note.id))

connect-metodia käytettäessä actionin dispatchaamiseen riittää

props.toggleImportanceOf(note.id)

Storen dispatch-funktiota ei enää tarvitse kutsua, sillä connect on muokannut action creatorin toggleImportanceOf sellaiseen muotoon, joka sisältää dispatchauksen.

mapDispatchToProps lienee aluksi hieman haastava ymmärtää, etenkin sen kohta käsiteltävä vaihtoehtoinen käyttötapa.

Connectin aikaansaamaa tilannetta voidaan havainnollistaa seuraavasti:

fullstack content

eli sen lisäksi että Notes pääsee storen tilaan propsien props.notes ja props.filter kautta, se viittaa props.toggleImportanceOf:lla funktioon, jonka avulla storeen saadaan dispatchattua TOGGLE_IMPORTANCE-tyyppisiä actioneja.

Connectia käyttämään refaktoroitu komponentti Notes on kokonaisuudessaan seuraava:

import React from 'react'
import { connect } from 'react-redux'
import Note from './Note'
import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = (props) => {
  const notesToShow = () => {
    if ( props.filter === 'ALL' ) {
      return props.notes
    }

    return props.filter === 'IMPORTANT'
      ? props.notes.filter(note => note.important)
      : props.notes.filter(note => !note.important)
  }

  return(
    <ul>
      {notesToShow().map(note =>
        <Note
          key={note.id}
          note={note}
          onClick={() => props.toggleImportanceOf(note.id)}
        />
      )}
    </ul>
  )
}

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const mapDispatchToProps = {
  toggleImportanceOf,
}

// eksportoidaan suoraan connectin palauttama komponentti
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

Otetaan connect käyttöön myös uuden muistiinpanon luomisessa:

import React from 'react'
import { connect } from 'react-redux'
import { createNote } from '../reducers/noteReducer'

const NewNote = (props) => {
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    props.createNote(content)
  }

  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

export default connect(
  null,
  { createNote }
)(NewNote)

Koska komponentti ei tarvitse storen tilasta mitään, on funktion connect ensimmäinen parametri null.

Sovelluksen tämänhetkinen koodi on githubissa branchissa part6-3.

Huomio propsina välitettyyn action creatoriin viittaamisesta

Tarkastellaan vielä erästä mielenkiintoista seikkaa komponentista NewNote:

import React from 'react'
import { connect } from 'react-redux'
import { createNote } from '../reducers/noteReducer'
const NewNote = (props) => {

  const addNote = (event) => {
    event.preventDefault()
    props.createNote(event.target.note.value)    event.target.note.value = ''
  }
  // ...
}

export default connect(
  null,
  { createNote }
)(NewNote)

Aloittelevalle connectin käyttäjälle aiheuttaa joskus ihmetystä se, että action creatorista createNote on komponentin sisällä käytettävissä kaksi eri versiota.

Funktioon tulee viitata propsien kautta, eli props.createNote, tällöin kyseessä on connectin muotoilema, dispatchauksen sisältävä versio funktiosta.

Moduulissa olevan import-lauseen

import { createNote } from './../reducers/noteReducer'

ansiosta komponentin sisältä on mahdollista viitata funktioon myös suoraan, eli createNote. Näin ei kuitenkaan tule tehdä, sillä silloin on kyseessä alkuperäinen action creator joka ei sisällä dispatchausta.

Jos tulostamme funktiot koodin sisällä (emme olekaan vielä käyttäneet kurssilla tätä erittäin hyödyllistä debug-kikkaa)

const NewNote = (props) => {
  console.log(createNote)
  console.log(props.createNote)

  const addNote = (event) => {
    event.preventDefault()
    props.createNote(event.target.note.value)
    event.target.note.value = ''
  }

  // ...
}

näemme eron:

fullstack content

Ensimmäinen funktioista siis on normaali action creator, toinen taas connectin muotoilema funktio, joka sisältää storen metodin dispatch-kutsun.

Connect on erittäin kätevä työkalu, mutta abstraktiutensa takia se voi aluksi tuntua hankalalta.

mapDispatchToPropsin vaihtoehtoinen käyttötapa

Määrittelimme siis connectin komponentille NewNote antamat actioneja dispatchaavan funktion seuraavasti:

const NewNote = () => {
  // ...
}

export default connect(
  null,
  { createNote }
)(NewNote)

Eli määrittelyn ansiosta komponentti dispatchaa uuden muistiinpanon lisäyksen suorittavan actionin suoraan komennolla props.createNote('uusi muistiinpano').

Parametrin mapDispatchToProps kenttinä ei voi antaa mitä tahansa funktiota, vaan funktion on oltava action creator, eli Redux-actionin palauttava funktio.

Kannattaa huomata, että parametri mapDispatchToProps on nyt olio, sillä määrittely

{
  createNote
}

on lyhempi tapa määritellä olioliteraali

{
  createNote: createNote
}

eli olio, jonka ainoan kentän createNote arvona on funktio createNote.

Voimme määritellä saman myös "pitemmän kaavan" kautta, antamalla connectin toisena parametrina seuraavanlaisen funktion:

const NewNote = (props) => {
  // ...
}

const mapDispatchToProps = dispatch => {  return {    createNote: value => {      dispatch(createNote(value))    },  }}
export default connect(
  null,
  mapDispatchToProps
)(NewNote)

Tässä vaihtoehtoisessa tavassa mapDispatchToProps on funktio, jota connect kutsuu antaen sille parametriksi storen dispatch-funktion. Funktion paluuarvona on olio, joka määrittelee joukon funktioita, jotka annetaan connectoitavalle komponentille propsiksi. Esimerkkimme määrittelee propsin createNote olevan funktion

value => {
  dispatch(createNote(value))
}

eli action creatorilla luodun actionin dispatchaus.

Komponentti siis viittaa funktioon propsin props.createNote kautta:

const NewNote = (props) => {

  const addNote = (event) => {
    event.preventDefault()
    props.createNote(event.target.note.value)
    event.target.note.value = ''
  }

  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

Konsepti on hiukan monimutkainen ja sen selittäminen sanallisesti on haastavaa. Useimmissa tapauksissa onneksi riittää mapDispatchToProps:in yksinkertaisempi muoto. On kuitenkin tilanteita, joissa monimutkaisempi muoto on tarpeen, esim. jos määriteltäessä propseiksi mäpättyjä dispatchattavia actioneja on viitattava komponentin omiin propseihin.

Egghead.io:sta löytyy Reduxin kehittäjän Dan Abramovin loistava tutoriaali Getting started with Redux, jonka katsomista voin suositella kaikille. Neljässä viimeisessä videossa käsitellään connect-metodia ja nimenomaan sen "hankalampaa" käyttötapaa.

Presentational/Container revisited

Komponentti Notes käyttää apumetodia notesToShow, joka päättelee filtterin perusteella näytettävien muistiinpanojen listan:

const Notes = (props) => {
  const notesToShow = () => {
    if ( props.filter === 'ALL' ) {
      return props.notes
    }

    return props.filter === 'IMPORTANT'
      ? props.notes.filter(note => note.important)
      : props.notes.filter(note => !note.important)
  }

  // ...
}

Komponentin on tarpeetonta sisältää kaikkea tätä logiikkaa. Eriytetään se komponentin ulkopuolelle connect-metodin parametrin mapStateToProps yhteyteen:

import React from 'react'
import { connect } from 'react-redux'
import Note from './Note'
import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = (props) => {
  return(
    <ul>
      {props.visibleNotes.map(note =>        <Note
          key={note.id}
          note={note}
          onClick={() => props.toggleImportanceOf(note.id)}
        />
      )}
    </ul>
  )
}

const notesToShow = ({ notes, filter }) => {  if (filter === 'ALL') {
    return notes
  }
  return filter === 'IMPORTANT'
    ? notes.filter(note => note.important)
    : notes.filter(note => !note.important)
}

const mapStateToProps = (state) => {
  return {
    visibleNotes: notesToShow(state),  }
}


const mapDispatchToProps = {
  toggleImportanceOf,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

mapStateToProps ei siis tällä kertaa mäppää propsiksi suoraan storessa olevaa asiaa, vaan storen tilasta funktion notesToShow avulla muodostetun sopivasti filtteröidyn datan. Uusi versio funktiosta notesToShow siis saa parametriksi koko tilan ja valitsee siitä sopivan osajoukon välitettäväksi komponentille. Tämänkaltaisia funktioita kutsutaan selektoreiksi.

Uudistettu Notes keskittyy lähes ainoastaan muistiinpanojen renderöimiseen, se on hyvin lähellä sitä minkä sanotaan olevan presentational-komponentti, joita Dan Abramovin sanoin kuvaillaan seuraavasti:

  • Are concerned with how things look.
  • May contain both presentational and container components inside, and usually have some DOM markup and styles of their own.
  • Often allow containment via props.children.
  • Have no dependencies on the rest of the app, such Redux actions or stores.
  • Don’t specify how the data is loaded or mutated.
  • Receive data and callbacks exclusively via props.
  • Rarely have their own state (when they do, it’s UI state rather than data).
  • Are written as functional components unless they need state, lifecycle hooks, or performance optimizations.

Connect-metodin avulla muodostettu yhdistetty komponentti

const notesToShow = ({notes, filter}) => {
  // ...
}

const mapStateToProps = (state) => {
  return {
    visibleNotes: notesToShow(state),
  }
}

const mapDispatchToProps = {
  toggleImportanceOf,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

taas on selkeästi container-komponentti, joita Dan Abramov luonnehtii seuraavasti:

  • Are concerned with how things work.
  • May contain both presentational and container components inside but usually don’t have any DOM markup of their own except for some wrapping divs, and never have any styles.
  • Provide the data and behavior to presentational or other container components.
  • Call Redux actions and provide these as callbacks to the presentational components.
  • Are often stateful, as they tend to serve as data sources.
  • Are usually generated using higher order components such as connect from React Redux, rather than written by hand.

Komponenttien presentational vs. container -jaottelu on eräs hyväksi havaittu tapa strukturoida React-sovelluksia. Jako voi olla toimiva tai sitten ei, kaikki riippuu kontekstista.

Abramov mainitsee jaon eduiksi muunmuassa seuraavat

  • Better separation of concerns. You understand your app and your UI better by writing components this way.
  • Better reusability. You can use the same presentational component with completely different state sources, and turn those into separate container components that can be further reused.
  • Presentational components are essentially your app’s “palette”. You can put them on a single page and let the designer tweak all their variations without touching the app’s logic. You can run screenshot regression tests on that page.

Abramov mainitsee termin high order component. Esim. Notes on normaali komponentti, React-reduxin connect metodi taas on high order komponentti, eli käytännössä funktio, joka haluaa parametrikseen komponentin muuttuakseen "normaaliksi" komponentiksi.

High order componentit eli HOC:t ovatkin yleinen tapa määritellä geneeristä toiminnallisuutta, joka sitten erikoistetaan esim. renderöitymisen määrittelyn suhteen parametrina annettavan komponentin avulla. Kyseessä on funktionaalisen ohjelmoinnin etäisesti olio-ohjelmoinnin perintää muistuttava käsite.

HOC:it ovat oikeastaan käsitteen High Order Function (HOF) yleistys. HOF:eja ovat sellaiset funkiot, jotka joko ottavat parametrikseen funktioita tai palauttavat funkioita. Olemme oikeastaan käyttäneet HOF:eja läpi kurssin, esim. lähes kaikki taulukoiden käsittelyyn tarkoitetut metodit, kuten map, filter ja find ovat HOF:eja.

Sovelluksen tämänhetkinen koodi on githubissa branchissa part6-4.

Huomaa muutokset kompnenteissa VisibilityFilter ja App