a Flux-arkkitehtuuri ja Reduxb Monta reduseria
    d connect

    c

    Redux-sovelluksen kommunikointi palvelimen kanssa

    Laajennetaan sovellusta siten, että muistiinpanot talletetaan backendiin. Käytetään osasta 2 tuttua json-serveriä.

    Tallennetaan projektin juuren tiedostoon db.json tietokannan alkutila:

    {
      "notes": [
        {
          "content": "the app state is in redux store",
          "important": true,
          "id": 1
        },
        {
          "content": "state changes are made with actions",
          "important": false,
          "id": 2
        }
      ]
    }

    Asennetaan projektiin json-server

    npm install json-server

    ja lisätään tiedoston package.json osaan scripts rivi

    "scripts": {
      "server": "json-server -p3001 --watch db.json",
      // ...
    }

    Käynnistetään json-server komennolla npm run server.

    Tehdään sitten tuttuun tapaan axiosia hyödyntävä backendistä dataa hakeva metodi tiedostoon services/notes.js

    import axios from 'axios'
    
    const baseUrl = 'http://localhost:3001/notes'
    
    const getAll = async () => {
      const response = await axios.get(baseUrl)
      return response.data
    }
    
    export default { getAll }

    Asennetaan myös axios projektiin

    npm install axios

    Muutetaan nodeReducer:issa tapahtuva muistiinpanojen tilan alustusta, siten että oletusarvoisesti muistiinpanoja ei ole:

    const noteReducer = (state = [], action) => {
      // ...
    };

    Nopea tapa saada storen tila alustettua palvelimella olevan datan perusteella on hakea muistiinpanot tiedostossa index.js ja dispatchata niille yksitellen action NEW_NOTE:

    // ...
    import noteService from './services/notes'
    const reducer = combineReducers({
      notes: noteReducer,
      filter: filterReducer,
    });
    
    const store = createStore(reducer);
    
    noteService.getAll().then(notes =>  notes.forEach(note => {    store.dispatch({ type: 'NEW_NOTE', data: note })  }))
    // ...

    Lisätään reduceriin tuki actionille INIT_NOTES, jonka avulla alustus voidaan tehdä dispatchaamalla yksittäinen action. Luodaan myös sitä varten oma action creator -funktio initializeNotes:

    // ...
    const noteReducer = (state = [], action) => {
      console.log('ACTION:', action)
      switch (action.type) {
        case 'NEW_NOTE':
          return [...state, action.data]
        case 'INIT_NOTES':      return action.data    // ...
      }
    }
    
    export const initializeNotes = (notes) => {
      return {
        type: 'INIT_NOTES',
        data: notes,
      }
    }
    
    // ...

    index.js yksinkertaistuu:

    import noteReducer, { initializeNotes } from './reducers/noteReducer'
    // ...
    
    noteService.getAll().then(notes =>
      store.dispatch(initializeNotes(notes))
    )

    HUOM: miksi emme käyttäneet koodissa promisejen ja then-metodilla rekisteröidyn tapahtumankäsittelijän sijaan awaitia?

    await toimii ainoastaan async-funktioiden sisällä, ja index.js:ssä oleva koodi ei ole funktiossa, joten päädyimme tilanteen yksinkertaisuuden takia tällä kertaa jättämään async:in käyttämättä.

    Päätetään kuitenkin siirtää muistiinpanojen alustus App-komponentiin, eli kuten yleensä dataa palvelimelta haettaessa, käytetään effect hookia:

    import React, {useEffect} from 'react'import NewNote from './components/NowNote'
    import Notes from './components/Notes'
    import VisibilityFilter from './components/VisibilityFilter'
    import noteService from './services/notes'
    import { initializeNotes } from './reducers/noteReducer'import { useDispatch } from 'react-redux'
    const App = () => {
      const dispatch = useDispatch()
      useEffect(() => {    noteService      .getAll().then(notes => dispatch(initializeNotes(notes)))  }, [])
      return (
        <div>
          <NewNote />
          <VisibilityFilter />
          <Notes />
        </div>
      )
    }
    
    export default App

    Hookin useEffect käyttö aiheuttaa eslint-varoituksen:

    fullstack content

    Pääsemme varoituksesta eroon seuraavasti:

    const App = () => {
      const dispatch = useDispatch()
      useEffect(() => {
        noteService
          .getAll().then(notes => dispatch(initializeNotes(notes)))
      }, [dispatch])
      // ...
    }

    Nyt komponentin App sisällä määritelty muuttuja dispatch eli käytännössä redux-storen dispatch-funktio on lisätty useEffectille parametrina annettuun taulukkoon. Jos dispatch-muuttujan sisältö muuttuisi ohjelman suoritusaikana, suoritettaisiin efekti uudelleen, näin ei kuitenkaan ole, eli varoitus on tässä tilanteessa oikeastaan aiheeton.

    Toinen tapa päästä eroon varoituksesta olisi disabloida se kyseisen rivin kohdalta:

    const App = () => {
      const dispatch = useDispatch()
      useEffect(() => {
        noteService
          .getAll().then(notes => dispatch(initializeNotes(notes)))   
      },[]) // eslint-disable-line react-hooks/exhaustive-deps  
      // ...
    }

    Yleisesti ottaen eslint-virheiden disabloiminen ei ole hyvä idea, joten vaikka kyseisen eslint-säännön tarpeellisuus onkin aiheuttanut kiistelyä, pitäydytään ylemmässä ratkaisussa.

    Lisää hookien riippuvuuksien määrittelyn tarpeesta reactin dokumentaatiossa.

    Voimme toimia samoin myös uuden muistiinpanon luomisen suhteen. Laajennetaan palvelimen kanssa kommunikoivaa koodia:

    const baseUrl = 'http://localhost:3001/notes'
    
    const getAll = async () => {
      const response = await axios.get(baseUrl)
      return response.data
    }
    
    const createNew = async (content) => {  const object = { content, important: false }  const response = await axios.post(baseUrl, object)  return response.data}
    export default {
      getAll,
      createNew,
    }

    Komponentin NewNote metodi addNote muuttuu hiukan:

    import React from 'react'
    import { useDispatch } from 'react-redux'
    import { createNote } from '../reducers/noteReducer'
    import noteService from '../services/notes'
    const NewNote = (props) => {
      const dispatch = useDispatch()
      
      const addNote = async (event) => {
        event.preventDefault()
        const content = event.target.note.value
        event.target.note.value = ''
        const newNote = await noteService.createNew(content)    dispatch(createNote(newNote))  }
    
      return (
        <form onSubmit={addNote}>
          <input name="note" />
          <button type="submit">add</button>
        </form>
      )
    }
    
    export default NewNote

    Koska backend generoi muistiinpanoille id:t, muutetaan action creator createNote muotoon

    export const createNote = (data) => {
      return {
        type: 'NEW_NOTE',
        data,
      }
    }

    Muistiinpanojen tärkeyden muuttaminen olisi mahdollista toteuttaa samalla periaatteella, eli tehdä palvelimelle ensin asynkroninen metodikutsu ja sen jälkeen dispatchata sopiva action.

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

    Asynkroniset actionit ja redux thunk

    Lähestymistapamme on ok, mutta siinä mielessä ikävä, että palvelimen kanssa kommunikointi tapahtuu komponenttien funktioissa. Olisi parempi, jos kommunikointi voitaisiin abstrahoida komponenteilta siten, että niiden ei tarvitsisi kuin kutsua sopivaa action creatoria, esim. App alustaisi sovelluksen tilan seuraavasti:

    const App = () => {
      const dispatch = useDispatch()
    
      useEffect(() => {
        dispatch(initializeNotes())
      },[dispatch]) 
      
      // ...
    }

    ja NoteForm loisi uuden muistiinpanon seuraavasti:

    const NewNote = () => {
      const dispatch = useDispatch()
      
      const addNote = async (event) => {
        event.preventDefault()
        const content = event.target.note.value
        event.target.note.value = ''
        dispatch(createNote(content))
      }
    
      // ...
    }

    Molemmat komponentit dispatchaisivat ainoastaan actionin, välittämättä siitä että taustalla tapahtuu todellisuudessa palvelimen kanssa tapahtuvaa kommunikointia.

    Asennetaan nyt redux-thunk-kirjasto, joka mahdollistaa asynkronisten actionien luomisen. Asennus tapahtuu komennolla:

    npm install redux-thunk

    redux-thunk-kirjasto on ns. redux-middleware joka täytyy ottaa käyttöön storen alustuksen yhteydessä. Eriytetään samalla storen määrittely omaan tiedostoon src/store.js:

    import { createStore, combineReducers, applyMiddleware } from 'redux'
    import thunk from 'redux-thunk'
    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(
        applyMiddleware(thunk)
      )
    )
    
    export default store

    Tiedosto src/index.js on muutoksen jälkeen seuraava

    import React from 'react'
    import ReactDOM from 'react-dom'
    import { Provider } from 'react-redux' 
    import store from './store'import App from './App'
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    )

    redux-thunkin ansiosta on mahdollista määritellä action creatoreja siten, että ne palauttavat funktion, jonka parametrina on redux-storen dispatch-metodi. Tämän ansiosta on mahdollista tehdä asynkronisia action creatoreja, jotka ensin odottavat jonkin toimenpiteen valmistumista ja vasta sen jälkeen dispatchaavat varsinaisen actionin.

    Voimme nyt määritellä muistiinpanojen alkutilan palvelimelta hakevan action creatorin initializeNotes seuraavasti:

    export const initializeNotes = () => {
      return async dispatch => {
        const notes = await noteService.getAll()
        dispatch({
          type: 'INIT_NOTES',
          data: notes,
        })
      }
    }

    Sisemmässä funktiossaan, eli asynkronisessa actionissa operaatio hakee ensin palvelimelta kaikki muistiinpanot ja sen jälkeen dispatchaa muistiinpanot storeen lisäävän actionin.

    Komponentti App voidaan nyt määritellä seuraavasti:

    const App = () => {
      const dispatch = useDispatch()
    
      useEffect(() => {    dispatch(initializeNotes())   },[dispatch]) 
      return (
        <div>
          <NewNote />
          <VisibilityFilter />
          <Notes />
        </div>
      )
    }

    Ratkaisu on elegantti, muistiinpanojen alustuslogiikka on eriytetty kokonaan React-komponenttien ulkopuolelle.

    Uuden muistiinpanon lisäävä action creator createNote on seuraavassa

    export const createNote = content => {
      return async dispatch => {
        const newNote = await noteService.createNew(content)
        dispatch({
          type: 'NEW_NOTE',
          data: newNote,
        })
      }
    }

    Periaate on jälleen sama, ensin suoritetaan asynkroninen operaatio, ja sen valmistuttua dispatchataan storen tilaa muuttava action.

    Komponentti NewNote muuttuu seuraavasti:

    const NewNote = () => {
      const dispatch = useDispatch()
      
      const addNote = async (event) => {
        event.preventDefault()
        const content = event.target.note.value
        event.target.note.value = ''
        dispatch(createNote(content))  }
    
      return (
        <form onSubmit={addNote}>
          <input name="note" />
          <button type="submit">lisää</button>
        </form>
      )
    }

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