Skip to content

c

Communicating with a server in a redux application

Let's expand our application so that the tasks are stored in the backend. We'll use json-server, familiar from part 2.

The initial state of the database is stored in the file db.json, which is placed in the root of the project:

{
  "tasks": [
    {
      "content": "learn more about how the app state is in redux store",
      "important": true,
      "id": 1
    },
    {
      "content": "understand more fully how state changes are made with actions",
      "important": false,
      "id": 2
    }
  ]
}

Let's install both axios and json-server to the project:

npm i -D json-server
npm i axios

Then add the following line to the scripts part of our package.json

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

We can now launch json-server with the command npm run server.

Setting up the application to work with a server

Next, in a new file services/tasks.js, we'll create a method getAll that uses axios to fetch data from the backend

import axios from 'axios'

const baseUrl = 'http://localhost:3001/tasks'

const getAll = async () => {
  const response = await axios.get(baseUrl)
  return response.data
}

export default { getAll }

We'll change the initialization of the state in taskReducer.js, so that by default there are no tasks:

const taskSlice = createSlice({
  name: 'tasks',
  initialState: [],  // ...
})

Let's also add a new action appendTask for adding a task object:

const taskSlice = createSlice({
  name: 'tasks',
  initialState: [],
  reducers: {
    createTask(state, action) {
      const content = action.payload

      state.push({
        content,
        important: false,
        id: generateId(),
      })
    },
    toggleImportanceOf(state, action) {
      const id = action.payload

      const taskToChange = state.find(t => t.id === id)

      const changedTask = { 
        ...taskToChange, 
        important: !taskToChange.important 
      }

      return state.map(task =>
        task.id !== id ? task : changedTask 
      )     
    },
    appendTask(state, action) {      state.push(action.payload)    }  },
})

export const { createTask, toggleImportanceOf, appendTask } = taskSlice.actions
export default taskSlice.reducer

A quick way to *initialize the tasks' state based on the server's data* is to *fetch the tasks* in main.jsx and dispatch an action using the appendTask action creator for each individual task object:

// ...
import taskService from './services/tasks'import taskReducer, { appendTask } from './reducers/taskReducer'
const store = configureStore({
  reducer: {
    tasks: taskReducer,
    filter: filterReducer,
  }
})

taskService.getAll().then(tasks =>  tasks.forEach(task => {    store.dispatch(appendTask(task))  }))
// ...

Further refactoring of our backend

Dispatching multiple appendTasks seems a bit impractical. Let's add an action creator setTasks which can be used to directly replace the tasks array. We'll get the action creator from the createSlice function by implementing the setTasks action:

// ...

const taskSlice = createSlice({
  name: 'tasks',
  initialState: [],
  reducers: {
    createTask(state, action) {
      const content = action.payload

      state.push({
        content,
        important: false,
        id: generateId(),
      })
    },
    toggleImportanceOf(state, action) {
      const id = action.payload

      const taskToChange = state.find(t => t.id === id)

      const changedTask = { 
        ...taskToChange, 
        important: !taskToChange.important 
      }

      return state.map(task =>
        task.id !== id ? task : changedTask 
      )     
    },
    appendTask(state, action) {
      state.push(action.payload)
    },
    setTasks(state, action) {      return action.payload    }  },
})

export const { createTask, toggleImportanceOf, appendTask, setTasks } = taskSlice.actions
export default taskSlice.reducer

Now, the code in main.jsx looks a lot better:

// ...
import taskService from './services/tasks'
import taskReducer, { setTasks } from './reducers/taskReducer'
const store = configureStore({
  reducer: {
    tasks: taskReducer,
    filter: filterReducer,
  }
})

taskService.getAll().then(tasks =>
  store.dispatch(setTasks(tasks)))

Pertinent: why didn't we use await in place of promises and event handlers (registered to then methods)?

await only works inside async functions, and taskService.getAll in main.jsx is not inside a function, so we'll abstain from using async here.

Let's refactor the task initialization into the App component, and, as usual, when fetching data from a server, we'll use the effect hook.

import NewTask from './components/NewTask'
import Tasks from './components/Tasks'
import VisibilityFilter from './components/VisibilityFilter'
import { useEffect } from 'react'import { useDispatch } from 'react-redux'import taskService from './services/tasks'import { setTasks } from './reducers/taskReducer'
const App = () => {
  const dispatch = useDispatch()  useEffect(() => {    taskService      .getAll().then(tasks => dispatch(setTasks(tasks)))  }, [])
  return (
    <div>
      <NewTask />
      <VisibilityFilter />
      <Tasks />
    </div>
  )
}

export default App

Sending data to the backend

We can do the same thing when it comes to creating a new task. Let's expand the code communicating with the server in services/tasks.js:

const baseUrl = 'http://localhost:3001/tasks'

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 addTask in components/NewTask.js changes slightly:

import { useDispatch } from 'react-redux'
import { createTask } from '../reducers/taskReducer'
import taskService from '../services/tasks'
const NewTask = (props) => {
  const dispatch = useDispatch()
  
  const addTask = async (event) => {    event.preventDefault()
    const content = event.target.task.value
    event.target.task.value = ''
    const newTask = await taskService.createNew(content)    dispatch(createTask(newTask))  }

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

export default NewTask

Because the backend generates ids for the tasks, we'll change the action creator createTask and remove generateId in the file taskReducer.js accordingly:

const taskSlice = createSlice({
  name: 'tasks',
  initialState: [],
  reducers: {
    createTask(state, action) {
      state.push(action.payload)    },
    // ..
  },
})

Changing the importance of tasks could be implemented using the same principle, by 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-4.

Asynchronous actions and Redux thunk

Our approach is pretty good, but we can improve the separation between the components and the server communication. It would be better if the communication could be abstracted away from the components so 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 = () => {
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch(initializeTasks()))  
  }, [dispatch]) 

  // ...
}

and NewTask would create a new task as follows:

const NewTask = () => {
  const dispatch = useDispatch()
  
  const addTask = async (event) => {
    event.preventDefault()
    const content = event.target.task.value
    event.target.task.value = ''
    dispatch(createTask(content))
  }

  // ...
}

In this implementation, both components would dispatch an action without the need to know about the communication between the server, which happens behind the scenes. These kinds of async actions can be implemented using the Redux Thunk library. The use of the library doesn't need any additional configuration/installation when we use configureStore.

With Redux Thunk it is possible to implement action creators which return a function instead of an object. The function receives Redux store's dispatch and getState methods as parameters. This allows implementations of asynchronous action creators, which:

  1. wait for the completion of a specific asynchronous operation
  2. dispatch an action, which changes the store's state.

We can define an action creator initializeTasks which initializes the tasks based on the data received from the server:

// ...
import taskService from '../services/tasks'
const taskSlice = createSlice(/* ... */)

export const { createTask, toggleImportanceOf, appendTask, setTasks } = taskSlice.actions

export const initializeTasks = () => {  return async dispatch => {    const tasks = await taskService.getAll()    dispatch(setTasks(tasks))  }}
export default taskSlice.reducer

In the inner function, meaning the asynchronous action, the operation first fetches all the tasks from the server (getAll) and then dispatches the setTasks action, which adds them to the store.

The component App can now be defined as follows:

// ...
import { initializeTasks } from './reducers/taskReducer'
const App = () => {
  const dispatch = useDispatch()

  useEffect(() => {    dispatch(initializeTasks())   }, [dispatch]) 
  return (
    <div>
      <NewTask />
      <VisibilityFilter />
      <Tasks />
    </div>
  )
}

The solution is elegant. The initialization logic for the tasks has been completely separated from the React component.

Refactoring to separate task creation

Next, let's replace the createTask action creator created by the createSlice function with an asynchronous action creator:

// ...
import taskService from '../services/tasks'

const taskSlice = createSlice({
  name: 'tasks',
  initialState: [],
  reducers: {
    // createTask definition removed from here!
    toggleImportanceOf(state, action) {
      const id = action.payload

      const taskToChange = state.find(t => t.id === id)

      const changedTask = { 
        ...taskToChange, 
        important: !taskToChange.important 
      }

      return state.map(task =>
        task.id !== id ? task : changedTask 
      )     
    },
    appendTask(state, action) {
      state.push(action.payload)
    },
    setTasks(state, action) {
      return action.payload
    }
  },
})

export const { toggleImportanceOf, appendTask, setTasks } = taskSlice.actions
export const initializeTasks = () => {
  return async dispatch => {
    const tasks = await taskService.getAll()
    dispatch(setTasks(tasks))
  }
}

export const createTask = content => {  return async dispatch => {    const newTask = await taskService.createNew(content)    dispatch(appendTask(newTask))  }}
export default taskSlice.reducer

The principle here is the same: first, an asynchronous operation (createNew) is executed, then the action changing the state of the store is dispatched.

The component NewTask changes as follows:

// ...
import { createTask } from '../reducers/taskReducer'
const NewTask = () => {
  const dispatch = useDispatch()
  
  const addTask = async (event) => {
    event.preventDefault()
    const content = event.target.task.value
    event.target.task.value = ''
    dispatch(createTask(content))  }

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

Finally, let's clean up the main.jsx file a bit by moving the code related to the creation of the Redux store into its own, store.js file:

import { configureStore } from '@reduxjs/toolkit'

import taskReducer from './reducers/taskReducer'
import filterReducer from './reducers/filterReducer'

const store = configureStore({
  reducer: {
    tasks: taskReducer,
    filter: filterReducer
  }
})

export default store

After the changes, the content of the main.jsx is the following:

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

The current state of the code for the application can be found on GitHub in the branch part6-5.

Redux Toolkit offers a multitude of tools to simplify asynchronous state management, like the createAsyncThunk function and the RTK Query API.