Core
Concepts
- 单一数据源: 整个应用程序的状态存储在单个对象树中 (容易跟踪/调试)
- 状态只读: 通过 dispatch(action) 间接更改状态, 不直接写入状态
- 纯函数更改状态: reducer(state, action) => newState
Store
Redux 中只有一个全局唯一 store 状态树, 且由 reducers 创建 store.
export default appStore = createStore(rootReducers, initState)
Creation
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot)
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
return store
}
Configuration
By default, configureStore from Redux Toolkit will:
- Call
applyMiddlewarewith a default list of middlewares- Async thunk middleware.
- Immutability check middleware: throw error when detecting mutations in reducers during a dispatch.
- Serializability check middleware: throw error when deeply detecting non-serializable values in state tree (functions, promises, symbols, and other non-plain-data values).
- Call
composeWithDevToolsto set up the Redux DevTools Extension.
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: [loggerMiddleware, ...getDefaultMiddleware()],
preloadedState,
})
if (process.env.NODE_ENV === 'development' && module.hot)
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
return store
}
State
在 Redux 中 State 并不显式定义:
- 初态与变化态皆由 Reducers 定义并控制
- Actions 中保存着 action.type 外, 还保存着供 Reducers 进行有效状态变化的其他信息(可自定义)
- 调用 Dispatch 方法自动向 Store 传递一个 Action(因为只有一个全局 Store, 故无需额外指定 Store 参数), Store 遍历调用其中的 Reducers, 根据 switch 语句进行匹配 action 处理
- reducer 只保存最基本的 state, 可计算出的 state 放在 mapStateToProps(selector) 中直接计算后绑定至 props
- 将数据保存在 Redux 存储中, 并在组件内部保持 UI 相关状态
Persistence
// localStorage.getItem('state')/localStorage.setItem('state', serializedState)
const persistedState = loadLocalStorageState()
const appStore = createStore(rootReducers, persistedState)
appStore.subscribe(
throttle(() => {
saveLocalStorageState({
todos: store.getState().todos,
})
}, 1000)
)
Normalization
Redux normalizing state shape:
- Only have one copy of each particular piece of data in state (no duplication).
- Normalized data is kept in lookup table (key-value store), where item IDs are keys, items themselves are values.
- There may also be an array of all of the IDs for a particular item type.
Normalizing data:
- Each type of data gets its own
tablein state. - Each
data tableshould store individual items in an{ key, value }object:"p1" : { id : "p1", author : "user1", comments : ["comment1", "comment2"] }. - Any references to individual items should be item ID.
- Arrays of IDs should be used to indicate ordering.
const state = {
users: {
ids: ['user1', 'user2', 'user3'],
entities: {
user1: { id: 'user1', firstName, lastName },
user2: { id: 'user2', firstName, lastName },
user3: { id: 'user3', firstName, lastName },
},
},
}
const userId = 'user2'
const userObject = state.users.entities[userId]
Normalize nesting data with Normalizr:
const data = {
entities: {
authors: { byId: {}, allIds: [] },
books: { byId: {}, allIds: [] },
authorBook: {
byId: {
1: {
id: 1,
authorId: 5,
bookId: 22,
},
2: {
id: 2,
authorId: 5,
bookId: 15,
},
3: {
id: 3,
authorId: 42,
bookId: 12,
},
},
allIds: [1, 2, 3],
},
},
}
const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....',
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....',
},
],
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....',
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....',
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....',
},
],
},
// and repeat many times
]
const normalizedBlogPosts = {
posts: {
byId: {
post1: {
id: 'post1',
author: 'user1',
body: '......',
comments: ['comment1', 'comment2'],
},
post2: {
id: 'post2',
author: 'user2',
body: '......',
comments: ['comment3', 'comment4', 'comment5'],
},
},
allIds: ['post1', 'post2'],
},
comments: {
byId: {
comment1: {
id: 'comment1',
author: 'user2',
comment: '.....',
},
comment2: {
id: 'comment2',
author: 'user3',
comment: '.....',
},
comment3: {
id: 'comment3',
author: 'user3',
comment: '.....',
},
comment4: {
id: 'comment4',
author: 'user1',
comment: '.....',
},
comment5: {
id: 'comment5',
author: 'user3',
comment: '.....',
},
},
allIds: ['comment1', 'comment2', 'comment3', 'comment4', 'comment5'],
},
users: {
byId: {
user1: {
username: 'user1',
name: 'User 1',
},
user2: {
username: 'user2',
name: 'User 2',
},
user3: {
username: 'user3',
name: 'User 3',
},
},
allIds: ['user1', 'user2', 'user3'],
},
}
Entity Adapter
- Build normalized state.
- Return normalized state CURD operation reducers.
- Get data selectors by
getSelectors.
import {
createAsyncThunk,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit'
import { client } from './api'
const postsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date),
})
// State = { ids: [], entities: {}, status: 'idle', error: null };
const initialState = postsAdapter.getInitialState({
status: 'idle',
error: null,
})
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await client.get('/fakeApi/posts')
return response.data
})
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.entities[postId]
if (existingPost)
existingPost.reactions[reaction]++
},
postUpdated(state, action) {
const { id, title, content } = action.payload
const existingPost = state.entities[id]
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
},
},
extraReducers(builder) {
builder
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
// Use the `upsertMany` reducer as a mutating update utility
postsAdapter.upsertMany(state, action.payload)
})
// Use the `addOne` reducer for the fulfilled case
.addCase(addNewPost.fulfilled, postsAdapter.addOne)
},
})
export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions
// Export the customized selectors for this adapter using `getSelectors`
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds,
// Pass in a selector that returns the posts slice of state
} = postsAdapter.getSelectors(state => state.posts)
export const selectPostsByUser = createSelector(
[selectAllPosts, (state, userId) => userId],
(posts, userId) => posts.filter(post => post.user === userId)
)
export default postsSlice.reducer