b
Monta reduseria
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:
Aloitetaan todella suoraviivaisella toteutuksella:
import React from 'react'
import NewNote from './components/NewNote'
import Notes from './components/Notes'
const App = () => {
const filterSelected = (value) => { console.log(value) }
return (
<div>
<NewNote />
<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> <Notes />
</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 { 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)
console.log(store.getState())
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
<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:
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:
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:
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(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Korjataan sitten bugi, joka johtuu siitä, että koodi olettaa storen tilan olevan mustiinpanot tallettava taulukko:
Korjaus on helppo. Koska muistiinpanot ovat nyt storen kentässä notes, riittää pieni muutos selektorifunktioon:
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(state => state.notes)
return(
<ul>
{notes.map(note =>
<Note
key={note.id}
note={note}
handleClick={() =>
dispatch(toggleImportanceOf(note.id))
}
/>
)}
</ul>
)
}
Aiemminhan selektorifunktio palautti koko storen tilan:
const notes = useSelector(state => state)
Nyt siis palautetaan tilasta ainoastaan sen kenttä notes
const notes = useSelector(state => state.notes)
Eriytetään näkyvyyden säätelyfiltteri omaksi, tiedostoon sijoitettavaksi src/components/VisibilityFilter.js komponentiksi:
import React from 'react'
import { filterChange } from '../reducers/filterReducer'
import { useDispatch } from 'react-redux'
const VisibilityFilter = (props) => {
const dispatch = useDispatch()
return (
<div>
all
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('ALL'))}
/>
important
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('IMPORTANT'))}
/>
nonimportant
<input
type="radio"
name="filter"
onChange={() => dispatch(filterChange('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 = () => {
return (
<div>
<NewNote />
<VisibilityFilter />
<Notes />
</div>
)
}
export default App
Muutetaan vielä komponentin Notes ottamaan huomioon filtteri
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(state => { if ( state.filter === 'ALL' ) { return state.notes } return state.filter === 'IMPORTANT' ? state.notes.filter(note => note.important) : state.notes.filter(note => !note.important) })
return(
<ul>
{notes.map(note =>
<Note
key={note.id}
note={note}
handleClick={() =>
dispatch(toggleImportanceOf(note.id))
}
/>
)}
</ul>
)
Muutos kohdistuu siis ainoastaan selektorifunktioon, joka oli aiemmin muotoa
useSelector(state => state.notes)
Yksinkertaistetaan vielä selektoria destrukturoimalla parametrina olevasta tilasta sen kentät erilleen:
const notes = useSelector(({ filter, notes }) => {
if ( filter === 'ALL' ) {
return notes
}
return filter === 'IMPORTANT'
? notes.filter(note => note.important)
: notes.filter(note => !note.important)
})
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.
Redux DevTools
Chromeen on asennettavissa Redux DevTools, jonka avulla Redux-storen tilaa ja sitä muuttavia actioneja on mahdollisuus seurata selaimen konsolista.
Selaimen lisäosan lisäksi debugatessa tarvitaan kirjastoa redux-devtools-extension. Asennetaan se komennolla
npm install --save-dev redux-devtools-extension
Storen luomistapaa täytyy hieman muuttaa, että kirjasto saadaan käyttöön:
// ...
import { createStore, combineReducers } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer
})
const store = createStore(
reducer,
composeWithDevTools())
export default store
Kun nyt avaat konsolin, välilehti redux näyttää seuraavalta:
Kunkin actionin storen tilaan aiheuttamaa muutosta on helppo tarkastella
Konsolin avulla on myös mahdollista dispatchata actioneja storeen
Sovelluksen tämänhetkinen koodi on githubissa branchissa part6-2.