目的
Redux Toolkit 它最初是为了帮助解决有关 Redux 的三个常见问题而创建的:
- “配置 Redux store 过于复杂”
- “我必须添加很多软件包才能开始使用 Redux”
- “Redux 有太多样板代码”
包含
configureStore()
:封装了createStore
,简化配置项,提供一些现成的默认配置项。它可以自动组合 slice 的 reducer,可以添加任何 Redux 中间件,默认情况下包含redux-thunk
,并开启了 Redux DevTools 扩展。createReducer()
帮你将 action type 映射到 reducer 函数,而不是编写 switch…case 语句。另外,它会自动使用immer
库来让你使用普通的 mutable 代码编写更简单的 immutable 更新,例如state.todos[3].completed = true
。createAction()
生成给定 action type 字符串的 action creator 函数。该函数本身已定义了toString()
,因此可以代替常量类型使用。createSlice()
接收一组 reducer 函数的对象,一个 slice 切片名和初始状态 initial state,并自动生成具有相应 action creator 和 action type 的 slice reducer。createAsyncThunk
: 接收一个 action type 字符串和一个返回值为 promise 的函数, 并生成一个 thunk 函数,这个 thunk 函数可以基于之前那个 promise ,dispatch 一组 type 为pending/fulfilled/rejected
的 action。createEntityAdapter
: 生成一系列可复用的 reducer 和 selector,从而管理 store 中的规范化数据。createSelector
来源于 Reselect 库,重新 export 出来以方便使用。
使用
仅展示目前使用到的,后续持续更新。
createSlice
一个函数,它接受初始状态、reducer函数的对象和一个“片名”(就是下面的name),并自动生成与reducer和state对应的动作创建者和动作类型。
demo:
import { createSlice } from '@reduxjs/toolkit'
const initialState = { value: 0 }
const counterSlice = createSlice({
name: 'counter',
initialState:{
count: 0,
status: false,
searchValue: ''
},
reducers: {
increment(state) {
state.count++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action) {
state.searchValue += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
参数介绍:
function createSlice({
// action中type的前缀
name: string,
// action中value的初始值
initialState: any,
// An object of "case reducers". Key names will be used to generate actions.
reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
// A "builder callback" function used to add more reducers, or
// an additional object of "case reducers", where the keys should be other
// action types
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})
定义回调:
import { createSlice, nanoid } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer: (state, action) => {
state.push(action.payload)
},
prepare: (text) => {
const id = nanoid()
return { payload: { id, text } }
},
},
},
})
extraReducers:
import { createAction, createSlice } from '@reduxjs/toolkit'
const incrementBy = createAction('incrementBy')
const decrement = createAction('decrement')
function isRejectedAction(action) {
return action.type.endsWith('rejected')
}
createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementBy, (state, action) => {
// action is inferred correctly here if using TS
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {})
// You can match a range of action types
.addMatcher(
isRejectedAction,
// `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard
(state, action) => {}
)
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
},
})
综合demo:
import { createSlice, createAction } from '@reduxjs/toolkit'
import { createStore, combineReducers } from 'redux'
const incrementBy = createAction('incrementBy')
const decrementBy = createAction('decrementBy')
const counter = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
multiply: {
reducer: (state, action) => state * action.payload,
prepare: (value) => ({ payload: value || 2 }), // fallback if the payload is a falsy value
},
},
// "builder callback API", recommended for TypeScript users
extraReducers: (builder) => {
builder.addCase(incrementBy, (state, action) => {
return state + action.payload
})
builder.addCase(decrementBy, (state, action) => {
return state - action.payload
})
},
})
const user = createSlice({
name: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
},
},
// "map object API"
extraReducers: {
[counter.actions.increment]: (
state,
action /* action will be inferred as "any", as the map notation does not contain type information */
) => {
state.age += 1
},
},
})
const reducer = combineReducers({
counter: counter.reducer,
user: user.reducer,
})
const store = createStore(reducer)
store.dispatch(counter.actions.increment())
// -> { counter: 1, user: {name : '', age: 21} }
store.dispatch(counter.actions.increment())
// -> { counter: 2, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply(3))
// -> { counter: 6, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply())
// -> { counter: 12, user: {name: '', age: 22} }
console.log(`${counter.actions.decrement}`)
// -> "counter/decrement"
store.dispatch(user.actions.setUserName('eric'))
// -> { counter: 12, user: { name: 'eric', age: 22} }
createSelector
React
组件使用useSelector
钩子从 store 读取数据选择器函数接收整个
state
对象,并且返回需要的部分数据每当Redux store 更新时,选择器将重新运行,如果它们返回的数据发生更改,则组件将重新渲染
demo:
const selectSelf = (state: State) => state
const unsafeSelector = createSelector(selectSelf, (state) => state.value)
const draftSafeSelector = createDraftSafeSelector(
selectSelf,
(state) => state.value
)
// in your reducer:
state.value = 1
const unsafe1 = unsafeSelector(state)
const safe1 = draftSafeSelector(state)
state.value = 2
const unsafe2 = unsafeSelector(state)
const safe2 = draftSafeSelector(state)
configureStore
配置Reducer的地方,将所有的Reducer管理起来
参数介绍:
type ConfigureEnhancersCallback = (
defaultEnhancers: StoreEnhancer[]
) => StoreEnhancer[]
interface ConfigureStoreOptions<
S = any,
A extends Action = AnyAction,
M extends Middlewares<S> = Middlewares<S>
> {
/**
* A single reducer function that will be used as the root reducer, or an
* object of slice reducers that will be passed to `combineReducers()`.
*/
reducer: Reducer<S, A> | ReducersMapObject<S, A>
/**
* An array of Redux middleware to install. If not supplied, defaults to
* the set of middleware returned by `getDefaultMiddleware()`.
*/
middleware?: ((getDefaultMiddleware: CurriedGetDefaultMiddleware<S>) => M) | M
/**
* Whether to enable Redux DevTools integration. Defaults to `true`.
*
* Additional configuration can be done by passing Redux DevTools options
*/
devTools?: boolean | DevToolsOptions
/**
* The initial state, same as Redux's createStore.
* You may optionally specify it to hydrate the state
* from the server in universal apps, or to restore a previously serialized
* user session. If you use `combineReducers()` to produce the root reducer
* function (either directly or indirectly by passing an object as `reducer`),
* this must be an object with the same shape as the reducer map keys.
*/
preloadedState?: DeepPartial<S extends any ? S : S>
/**
* The store enhancers to apply. See Redux's `createStore()`.
* All enhancers will be included before the DevTools Extension enhancer.
* If you need to customize the order of enhancers, supply a callback
* function that will receive the original array (ie, `[applyMiddleware]`),
* and should return a new array (such as `[applyMiddleware, offline]`).
* If you only need to add middleware, you can use the `middleware` parameter instead.
*/
enhancers?: StoreEnhancer[] | ConfigureEnhancersCallback
}
function configureStore<S = any, A extends Action = AnyAction>(
options: ConfigureStoreOptions<S, A>
): EnhancedStore<S, A>
demo:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({ reducer: rootReducer })
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
full demo:
// file: todos/todosReducer.ts noEmit
import type { Reducer } from '@reduxjs/toolkit'
declare const reducer: Reducer<{}>
export default reducer
// file: visibility/visibilityReducer.ts noEmit
import type { Reducer } from '@reduxjs/toolkit'
declare const reducer: Reducer<{}>
export default reducer
// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
// We'll use redux-logger just as an example of adding another middleware
import logger from 'redux-logger'
// And use redux-batched-subscribe as an example of adding enhancers
import { batchedSubscribe } from 'redux-batched-subscribe'
import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'
const reducer = {
todos: todosReducer,
visibility: visibilityReducer,
}
const preloadedState = {
todos: [
{
text: 'Eat food',
completed: true,
},
{
text: 'Exercise',
completed: false,
},
],
visibilityFilter: 'SHOW_COMPLETED',
}
const debounceNotify = _.debounce(notify => notify());
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
enhancers: [batchedSubscribe(debounceNotify)],
})
// The store has been created with these options:
// - The slice reducers were automatically passed to combineReducers()
// - redux-thunk and redux-logger were added as middleware
// - The Redux DevTools Extension is disabled for production
// - The middleware, batched subscribe, and devtools enhancers were composed together
useDispatch
相当于传统的disPatch一样,将action行为发到store。
import { useDispatch } from "react-redux"
//...
const dispatch = useDispatch()
const setCharacter = (cha) => dispatch(descripeHer(cha))
onClick={() => {
setCharacter('so cool')
}}