跳到内容

c

在Redux应用中与后端通信

让我们扩展应用,使笔记被存储到后端。我们将使用json-server,这在第二章节中已经很熟悉。

数据库的初始状态被存储在文件db.json中,它被放置在项目的根部。

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

我们将为项目安装json-server...

npm install json-server --save-dev

然后在文件package.jsonscripts部分添加以下一行

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

现在让我们用命令npm run server来启动json-server。

接下来我们将在文件services/notes.js中创建一个方法,使用axios从后端获取数据。

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 }

我们将axios添加到项目中

npm install axios

我们将改变noteReducer中状态的初始化,这样默认情况下就没有笔记了。

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

我们还要添加一个新的动作appendNote来添加一个笔记对象。

const noteSlice = createSlice({
  name: 'notes',
  initialState,
  reducers: {
    createNote(state, action) {
      const content = action.payload

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

      const noteToChange = state.find(n => n.id === id)

      const changedNote = {
        ...noteToChange,
        important: !noteToChange.important
      }

      return state.map(note =>
        note.id !== id ? note : changedNote
      )
    },
    appendNote(state, action) {      state.push(action.payload)    }  },
})

export const { createNote, toggleImportanceOf, appendNote } = noteSlice.actions
export default noteSlice.reducer

基于从服务器收到的数据初始化笔记状态的快速方法是在index.js文件中获取笔记,并使用appendNote动作创建器为每个单独的笔记对象分派一个动作。

// ...
import noteService from './services/notes'import noteReducer, { appendNote } from './reducers/noteReducer'
const store = configureStore({
  reducer: {
    notes: noteReducer,
    filter: filterReducer,
  }
})

noteService.getAll().then(notes =>  notes.forEach(note => {    store.dispatch(appendNote(note))  }))
// ...

派遣多个动作似乎有点不切实际。让我们添加一个动作创建器setNotes,可以用来直接替换笔记数组。我们将通过实现setNotes动作,从createSlice函数中获得动作创建器。

// ...

const noteSlice = createSlice({
  name: 'notes',
  initialState,
  reducers: {
    createNote(state, action) {
      const content = action.payload

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

      const noteToChange = state.find(n => n.id === id)

      const changedNote = {
        ...noteToChange,
        important: !noteToChange.important
      }

      return state.map(note =>
        note.id !== id ? note : changedNote
      )
    },
    appendNote(state, action) {
      state.push(action.payload)
    },
    setNotes(state, action) {      return action.payload    }  },
})

export const { createNote, toggleImportanceOf, appendNote, setNotes } = noteSlice.actions
export default noteSlice.reducer

现在,index.js文件中的代码看起来好多了。

// ...
import noteService from './services/notes'
import noteReducer, { setNotes } from './reducers/noteReducer'
const store = configureStore({
  reducer: {
    notes: noteReducer,
    filter: filterReducer,
  }
})

noteService.getAll().then(notes =>
  store.dispatch(setNotes(notes)))

NB:我们为什么不用await来代替 promise 和事件处理程序(注册到then-methods)?

等待只在async函数中起作用,而index.js中的代码不在函数中,所以由于操作的简单性,我们这次就不使用async了。

然而,我们决定将笔记的初始化移到App组件中,并且,像往常一样,当从服务器获取数据时,我们将使用effect hook

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

export default App

使用useEffect钩子会导致一个eslint警告。

fullstack content

我们可以通过下面的操作摆脱它。

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

现在我们在App组件中定义的变量dispatch,实际上是redux-store的调度函数,已经被添加到useEffect的数组中作为参数接收。

如果调度变量的值在运行时发生变化。

该效果将被再次执行。然而这不会发生在我们的应用中,所以这个警告是不必要的。

摆脱警告的另一个方法是在这一行禁用eslint。

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

一般来说,当eslint抛出一个警告时禁用它并不是一个好主意。尽管有关的eslint规则引起了一些争论,我们将使用第一个解决方案。

更多关于需要定义钩子的依赖关系,请看 React文档

当涉及到创建一个新的笔记时,我们可以做同样的事情。让我们扩展一下与服务器通信的代码,如下。

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,
}

组件NewNoteaddNote方法略有变化。

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

因为后端为笔记生成了ID,我们将相应地改变动作创建者createNote

createNote(state, action) {
  state.push(action.payload)
}

改变笔记的重要性可以用同样的原则来实现,通过对服务器进行异步方法调用,然后分派一个适当的动作。

该应用的代码的当前状态可以在GitHub的分支part6-3中找到。

Asynchronous actions and redux thunk

我们的方法很好,但与服务器的通信发生在组件的功能中,这不是很好。如果能将通信从组件中抽象出来就更好了,这样它们就不必做任何其他事情,只需调用相应的动作创建器。举个例子,App将初始化应用的状态,如下所示。

const App = () => {
  const dispatch = useDispatch()

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

  // ...
}

NewNote将创建一个新的笔记,如下所示。

const NewNote = () => {
  const dispatch = useDispatch()

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

  // ...
}

在这个实现中,两个组件都会派发一个动作,而不需要知道幕后发生的服务器之间的通信。这类async动作可以使用Redux Thunk库来实现。当使用Redux工具包的configureStore函数创建Redux商店时,使用该库不需要任何额外配置。

现在让我们安装该库

npm install redux-thunk

通过Redux Thunk可以实现action creators,它返回一个函数而不是一个对象。该函数接收Redux存储的dispatchgetState方法作为参数。这允许异步动作创建者的实现,它首先等待某个异步操作的完成,然后分派一些动作,改变商店的状态。

我们可以定义一个动作创建器initializeNotes,根据从服务器收到的数据初始化笔记。

// ...
import noteService from '../services/notes'
const noteSlice = createSlice(/* ... */)

export const { createNote, toggleImportanceOf, setNotes, appendNote } = noteSlice.actions

export const initializeNotes = () => {  return async dispatch => {    const notes = await noteService.getAll()    dispatch(setNotes(notes))  }}
export default noteSlice.reducer

在内部函数中,指的是异步操作,该操作首先从服务器获取所有笔记,然后分派setNotes操作,将它们添加到存储中。

组件App现在可以被定义如下。

const App = () => {
  const dispatch = useDispatch()

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

这个解决方案很优雅。笔记的初始化逻辑已经完全从React组件中分离出来。

接下来,让我们用一个异步的动作创建器来取代由createSlice函数创建的createNote动作创建器。

// ...
import noteService from '../services/notes'

const noteSlice = createSlice({
  name: 'notes',
  initialState,
  reducers: {
    toggleImportanceOf(state, action) {      const id = action.payload      const noteToChange = state.find(n => n.id === id)      const changedNote = {        ...noteToChange,        important: !noteToChange.important      }      return state.map(note =>        note.id !== id ? note : changedNote      )    },    appendNote(state, action) {      state.push(action.payload)    },    setNotes(state, action) {      return action.payload    }  },
})

export const { toggleImportanceOf, appendNote, setNotes } = noteSlice.actions
export const initializeNotes = () => {
  return async dispatch => {
    const notes = await noteService.getAll()
    dispatch(setNotes(notes))
  }
}

export const createNote = content => {  return async dispatch => {    const newNote = await noteService.createNew(content)    dispatch(appendNote(newNote))  }}
export default noteSlice.reducer

这里的原理是一样的:首先,执行一个异步操作,之后,改变存储状态的动作被dispatched。Redux工具包提供了大量的工具来简化异步状态管理。适合这个用例的工具有:createAsyncThunk函数和RTK Query API。

组件NewNote的变化如下。

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>
  )
}

最后,让我们清理一下index.js文件,把与创建Redux商店有关的代码移到自己的store.js文件中。

import { configureStore } from '@reduxjs/toolkit'

import noteReducer from './reducers/noteReducer'
import filterReducer from './reducers/filterReducer'

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

export default store

更改后,index.js的内容如下。

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import store from './store'import App from './App'

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

应用的代码的当前状态可以在GitHub的分支part6-4中找到。