c
Communicating with server in a redux application
Let's expand the application, such that the notes are stored to the backend. We'll use json-server, familiar from part 2.
The initial state of the database is stored into the file db.json, which is placed in the root of the project:
{
"notes": [
{
"content": "the app state is in redux store",
"important": true,
"id": 1
},
{
"content": "state changes are made with actions",
"important": false,
"id": 2
}
]
}
We'll install json-server for the project...
npm install json-server --save
and add the following line to the scripts part of the file package.json
"scripts": {
"server": "json-server -p3001 db.json",
// ...
}
Now let's launch json-server with the command npm run server.
Next we'll create a method into the file services/notes.js, which uses axios to fetch data from the backend
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 }
We'll add axios to the project
npm install axios --save
We'll change the initialization of the state in noteReducer, such that by default there are no notes:
const noteReducer = (state = [], action) => {
// ...
};
A quick way to initialize the state based on the data on the server is to fetch the notes in the file index.js and dispatch the action NEW_NOTE for each of them:
// ...
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 }) }))
// ...
Let's add support in the reducer for the action INIT_NOTES, using which the initialization can be done by dispatching a single action. Let's also create an action creator function 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 simplifies:
import noteReducer, { initializeNotes } from './reducers/noteReducer'
// ...
noteService.getAll().then(notes =>
store.dispatch(initializeNotes(notes))
)
NB: why didn't we use await in place of promises and event handlers (registered to then-methods)?
Await only works inside async functions, and the code in index.js is not inside a function, so due to the simple nature of the operation, we'll abstain from using async this time.
We do, however, decide to move the initialization of the notes into the App component, and, as usual when fetching data from a server, we'll use the effect hook.
To get the action creator initializeNotes into use in the component App we will once again need the help of the connect-method:
import React, { useEffect } from 'react'import { connect } from 'react-redux'import NewNote from './components/NewNote'
import Notes from './components/Notes'
import VisibilityFilter from './components/VisibilityFilter'
import noteService from './services/notes'import { initializeNotes } from './reducers/noteReducer'
const App = (props) => {
useEffect(() => { noteService .getAll().then(notes => props.initializeNotes(notes)) },[])
return (
<div>
<NewNote />
<VisibilityFilter />
<Notes />
</div>
)
}
export default connect(null, { initializeNotes })(App)
This way the function initializeNotes becomes a prop, in the form of props.initializeNotes, for the component App, and when calling it we won't need the dispatch-method, because connect handles this for us.
We can do the same thing when it comes to creating a new note. Let's expand the code communicating with the server as follows:
import axios from 'axios'
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,}
The method addNote of the component NoteForm changes slightly:
import React from 'react'
import { connect } from 'react-redux'
import { createNote } from '../reducers/noteReducer'
import noteService from '../services/notes'
const NewNote = (props) => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
const newNote = await noteService.createNew(content) props.createNote(newNote) }
return (
// ...
)
}
export default connect(null, { createNote } )(NewNote)
Because the backend generates ids for the notes, we'll change the action creator createNote
export const createNote = (data) => {
return {
type: 'NEW_NOTE',
data,
}
}
Changing the importance of notes could be implemented using the same principle, meaning making an asynchronous method call to the server and then dispatching an appropriate action.
The current state of the code for the application can be found on github in the branch part6-5.
Asynchronous actions and redux thunk
Our approach is OK, but it is not great that the communication with the server happens inside the functions of the components. It would be better if the communication could be abstracted away from the components, such that they don't have to do anything else but call the appropriate action creator. As an example, App would initialize the state of the application as follows:
const App = (props) => {
useEffect(() => {
props.initializeNotes(notes)
},[])
// ...
}
and NoteForm would create a new note as follows:
const NewNote = (props) => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
props.createNote(content)
}
Both components would only use the function provided to them as a prop without caring about the communication with the server that is happening in the background.
Now let's install the redux-thunk-library, which enables us to create asynchronous actions. Installation is done with the command:
npm install --save redux-thunk
The redux-thunk-library is a so-called redux-middleware, which must be initialized along with the initialization of the store. While we're here, let's extract the definition of the store into its own file src/store.js:
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk';
import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer,
})
const store = createStore(reducer, applyMiddleware(thunk))
export default store
After the changes the file src/index.js looks like this
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'))
Thanks to redux-thunk, it is possible to define action creators so that they return a function having the dispatch-method of redux-store as its parameter. As a result of this, one can make asynchronous action creators, which first wait for some operation to finish, after which they then dispatch the real action.
Now we can define the action creator, initializeNotes, that initializes the state of the notes as follows:
export const initializeNotes = () => {
return async dispatch => {
const notes = await noteService.getAll()
dispatch({
type: 'INIT_NOTES',
data: notes,
})
}
}
In the inner function, meaning the asynchronous action, the operation first fetches all the notes from the server and then dispatches the notes to the action, which adds them to the store.
The component App can now be defined as follows:
const App = (props) => {
useEffect(() => {
props.initializeNotes()
},[])
return (
<div>
<NewNote />
<VisibilityFilter />
<Notes />
</div>
)
}
export default connect(
null, { initializeNotes }
)(App)
The solution is elegant. The initialization logic for the notes has been completely separated to outside the React component.
The action creator createNew, which adds a new note looks like this
export const createNote = content => {
return async dispatch => {
const newNote = await noteService.createNew(content)
dispatch({
type: 'NEW_NOTE',
data: newNote,
})
}
}
The principle here is the same: first an asynchronous operation is executed, after which the action changing the state of the store is dispatched.
The component NewNote changes as follows:
const NewNote = (props) => {
const addNote = async (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)
The current state of the code for the application can be found on github in the branch part6-6.
Redux DevTools
There is an extension Redux DevTools that can be installed on Chrome, in which the state of the Redux-store and the action that changes it can be monitored from the console of the browser.
When debugging, in addition to the browser extension we also have the software library redux-devtools-extension. Let's install it using the command:
npm install --save redux-devtools-extension
We'll have to slightly change the definition of the store to get the library up and running:
// ...
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
Now when you open the console, the redux tab looks like this:
It's also possible to dispatch actions to the store using the console
Redux and component state
We have come a long way in this course and, finally, we have come to the point at which we are using React "the right way", meaning React only focuses on generating the views, and the application state is separated completely from the React components and passed on to Redux, its actions, and its reducers.
What about the useState-hook, which provides components with their own state? Does it have any role if an application is using Redux or some other external state management solution? If the application has more complicated forms, it may be beneficial to implement their local state using the state provided by the useState function. One can, of course, have Redux manage the state of the forms, however, if the state of the form is only relevant when filling the form (e.g. for validation) it may be wise to leave the management of state to the component responsible for the form.
The final version of the code for the application can be found on github in the branch part6-7.