React 系列之基础二
React 状态管理
1.Flux 架构与 Redux
在 Flux 中, 状态完全从 React-components 分离到自己的存储中. 存储中的状态不会直接更改, 而是使用不同的 actions 进行更改.
当一个操作改变了存储的状态时, 视图会被重新渲染
使用以下命令安装 redux
npm install redux --save
Action 对 应用程序的影响是通过使用一个 reducer 来定义的. 实际上, reducer 是一个函数, 它以当前状态state和action为参数. 并返回一个新状态.
2. redux api
- createStore
参数是: reducer 实例, 例如 下面的 counterReducer
返回一个 store 实例 - store.dispatch
参数是 action 对象, {type:“xxx”}
将这个 action 分派到 store 中 - store.getState
查询 store 的状态 - store.subscribe
订阅函数, 参数是一个 回调函数;
它用于在 store 状态改变时 调用回调函数
import { createStore } from 'redux'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
case 'ZERO':
return 0
default:
return state
}
return state
}
const store = createStore(counterReducer)
console.log(store.getState())
store.dispatch({type: 'INCREMENT'})
console.log(store.getState())
// 订阅
store.subscribe(() => {
const storeNow = store.getState()
console.log("回调函数打印: ", storeNow)
})
store.dispatch({type: 'INCREMENT'})
export default counterReducer
a.当 store 的状态发生更改时, React 无法重新渲染组件. 因此我们可以注册一个renderApp, 让它在状态更改时被调用. 重新渲染 App.
b.上述 counterReducer.js 只是定义了一个改变状态的函数, 在使用 counterReducer 时, 需要在 dispatch 函数中创建 一个 action 实例, 然后传入 dispatch 函数.
一种更好的实践是, 在reducer中也把创建 action的方法添加到 reducer 中.
3. 纯函数, 不可变
Reducer 状态必须由不可变 immutable 对象组成. 如果状态发生更改. 则不会更改旧对象, 而是将其替换为 新的, 已更改的对象.
- 安装 deep-freeze 库, 它可以用来确保 reducer 被正确定义为不可变函数
npm install --save-dev deep-freeze - 创建 src/reducers/noteReducer.test.js
- 使用 deepFreeze(state) 确保 reducer 不会更改作为参数提供给它的存储状态
import noteReducer from './noteReducer'
import deepFreeze from 'deep-freeze'
describe('noteReducer', () => {
test('returns new state with action NEW_NOTE', () => {
const state = []
const action = {
type: 'NEW_NOTE',
data: {
content: 'the app state is in redux store',
important: true,
id: 1
}
}
// deepFreeze 确保 reducer 不会更改作为参数提供给它的存储的状态
deepFreeze(state)
const newState = noteReducer(state, action)
expect(newState).toHaveLength(1)
expect(newState).toContainEqual(action.data)
})
})
4. Redux-store 到多组件
问题: 前面使用 redux 管理状态的方式, 需要将 createStore 方法创建的 store 变量定义在所有组件都能共享到的地方. 这样才能让需要用到这个 状态的组件都能使用. 但是这样是不好的; 另一个原因, store 状态改变 不会导致页面重新渲染
答案: 目前最新最简单的方式是通过 react-redux 的 hooks-api
- step1: 安装 react-redux
npm install --save react-redux
- step2: 组件定义为 Provider 的子组件
将 App.js 定义出来, 并且它是 Provider 组件的子组件, 如下, 并在 Provider 传入 store, 这样在 App.js 组件内部 就能获取到 counter 状态 了.
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './App'
import counterReducer from './counterReducer'
// 通过 reducer 创建 store
const store = createStore(counterReducer)
ReactDOM.render(
// store 传入
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
- step3: 在 App.js 内 使用 useDispatch 和 useSelect
useDispatch: 返回当前 store 的 dispatch 对象, 可以通过它传递 action
useSelector: 返回当前的状态对象, 参数可以过滤需要的 状态属性
import { useSelector, useDispatch } from 'react-redux'
...
const App = () => {
const dispatch = useDispatch()
// 将 state 内的所有状态属性返回
const counter = useSelector(state => state)
...
// 使用 dispatch(action)
dispatch(changeCounter())
return ...
}
5. Combined Reducers(复合reducer)
假设现在有两个 reducer, 我们需要结合这两个 reducer.
- 使用 combineReducers 函数组合 reducer
import { createStore, combineReducers } from 'redux'
...
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer
})
const store = createStore(reducer)
此时这个 store 的状态是 {filter: “IMPORTANT”, notes: []}.
我们可以传递 noteReducer的 action 或 filterReducer 的action, store.dispatch 都能接受并修改自身的状态.
6.Redux DevTools
有一个扩展 Redux DevTools 可以安装到 Chrome 上, 其中 Redux-store的状态和改变它的 action 可以在浏览器的控制台上监控.
在调试时, 除了浏览器扩展外, 还需要软件库: redux-devtools-extension.
npm install --save-dev redux-devtools-extension
然后在创建 store 时添加如下代码:
import { composeWithDevTools } from 'redux-devtools-extension'
...
const store = createStore(
reducer,
composeWithDevTools()
)
...
- async 与 await
async 可以加载函数前, await 用于 async 函数内, 表示等待直到这一行执行完成. 例如:
const getAll = async () => {
const response = await axios.get(baseUrl)
return response.data
}
7. Asynchronous actions and redux thunk
7.1 原先的与服务器交互方式
...
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))
}
7.2 尝试 通过 async 和 await 在 reducer.js 中 与服务器交互
export const initialNotes = async () => {
// 从服务器拿到notes
let initNoteList = await getAll()
return {
type: 'INIT',
data: initNoteList
}
}
这个 initialNotes 函数返回对象并不能直接使用在 dispatch 上, 会出现如下错误
Error: Actions must be plain objects. Use custom middleware for async actions
原因在于:
这个 initialNotes 函数返回的值是 Promise 类型. 并不是一个直接的action .
解决:
感谢 redux thunk, 让我们可以解决这个问题
7.3 异步 action 和 redux thunk
我们的方法是可行的,但是与服务器的通信发生在组件的功能内部并不是很好。 如果能够将通信从组件中抽象出来就更好了,这样它们就不必做任何其他事情,只需调用适当的action creator。
例如,App 将应用的状态初始化如下
const App = () => {
const dispatch = useDispatch()
useEffect(()=>{
dispatch(initializeNotes())
}, [])
// ...我们希望获取到 服务器的所有note信息后, 再从 initializeNotes 函数返回.
}
const NewNote = () => {
const dispatch = useDispatch()
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.note.value = ''
dispatch(createNote(content))
}
// ...我们希望 createNote 成功 发送一个创建note的post 请求给后端并返回后, 调用dispatch
}
7.4 redux-thunk 库
它允许我们创建 asynchronous actions:
npm install --save redux-thunk
Redux-thunk-库 是所谓的 redux-中间件, 它必须在 store 的初始化过程中初始化.
- 此处将 store 的创建 提取到它自己的文件 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
- 单独建立 store.js 之后, 在 index.js 中
<Provider store={store}>
<App/>
</Provider>
- 在 reducer.js 中 定义 action 创建器 initializeNotes,
export const initializeNotes = () => {
return async dispatch => {
const notes = await noteService.getAll()
dispatch({type:'INIT_NOTES', data: notes})
}
}
这里与 7.2不同在于, 7.2 导出的 initializeNotes 函数是 async函数. 这里的 initializeNotes 函数返回值是一个异步函数
4. 现在 在 App.js 中可以定义如下:
const dispatch = useDispatch()
useEffect(()=>{
// 获取所有 note, 修改状态
dispatch(initializeNotes())
}, [])
如愿以偿的在 reducer 中与服务器交互, 而不是在组件中
8. connect 方法
到目前为止, 我们已经使用了 redux-store, 借助了 redux 中的 hook-api.
就是使用了 useSelector 和 useDispatch 函数.
这一章节, 我们将研究一种 redux 的另一种 更古老, 复杂的方法. redux 提供的 connect 函数.
8.1 使用 connect 函数将 redux 存储共享给组件
- 我们先来看一下使用 hook-api (useDispatch 和 useSelector 函数) 的方式
Notes.js
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { toggleImportanceOf } from '../reducers/noteReducer'
const Notes = () => {
const dispatch = useDispatch()
const notes = useSelector(({filter, notes}) => {
if ( filter === 'ALL' ) {
return notes
}
return filter === 'IMPORTANT'
? notes.filter(note => note.important)
: notes.filter(note => !note.important)
})
return(
<ul>
{notes.map(note =>
<Note
key={note.id}
note={note}
handleClick={() =>
dispatch(toggleImportanceOf(note.id))
}
/>
)}
</ul>
)
}
export default Notes
- Connect 函数可用于转换 “常规” React 组件, 以便将 Redux 存储的状态 “映射” 到组件的 props 中.
connect 函数接受所谓的 mapStateToProps 函数作为它的第一个参数. 这个函数用来定义 基于 Redux 存储状态的连接组件的 props.
connect 函数的第二个参数 可用于定义 mapDispatchToProps, 它是一组作为 props 传递给连接组件的 action creator 函数.
Notes.js
import React from 'react'
import { connect } from 'react-redux'
import { toggleImportanceOf } from '../reducers/noteReducer'
const Notes = (props) => {
// 从 props 中获取当前状态
const notesToShow = () => {
if ( props.filter === 'ALL ') {
return props.notes
}
return props.filter === 'IMPORTANT'
? props.notes.filter(note => note.important)
: props.notes.filter(note => !note.important)
}
return (
<ul>
{props.notes.map(note =>
<Note
key={note.id}
note={note}
handleClick={() => props.toggleImportanceOf(note.id)}
/>
)}
</ul>
)
}
const mapStateToProps = (state) => {
return {
notes: state.notes,
filter: state.filter,
}
}
const mapDispatchToProps = {
// action creator 函数
toggleImportanceOf,
}
const ConnectedNotes = connect(
mapStateToProps,
mapDispatchToProps
)(Notes)
export default ConnectedNotes
这意味着 通过自己的 props 调用函数 toggleImportanceOf, 而不再需要 分派 action:
dispatch(toggleImportanceOf(note.id))
当使用 connect 时, 如果需要改变状态:
props.toggleImportanceOf(note.id)
8.2 Alternative way of using mapDispatchToProps
一种使用 mapDispatchToProps 的代替方法
mapDispatchToProps 参数是一个 Java Script object, 作为定义:
{
createNote: createNote
}
// 它是一个具有单个 createNote 属性的对象
或者, 可以将下面的 function 作为 connect 的第二个参数:
const mapDispatchToProps = dispatch => {
return {
createNote: value => {
dispatch(createNote(value))
},
}
}
export default connect(null, mapDispatchToProps)(NewNote)
在这个代替方案中, mapDispatchToProps 是一个函数, 函数的返回值是一个对象, 它定义了一组 作为props 传递给连接组件的函数.