Skip to main content

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 applyMiddleware with a default list of middlewares
  • Call composeWithDevTools to 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 table in state.
  • Each data table should 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

createEntityAdapter:

  • 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