目录
(1)使用dispatch()后发现state打印出来没有立即更新
(2)在reducer里没有返回新的对象或数组会导致react跳过这次dispatch
(1)当使用多个context.provider标签包裹子组件时
(一)useReducer
1.什么是useReducer?
useReducer是一个用于集中管理状态的 react Hook,通过dispatch函数派发类型和数据给actions函数,达到修改state的目的。
它接收一个reducer函数、state初始值、初始化参数init(可选),返回一个state数据和dispatch函数,相当于是useState的plus版,reducer函数就是我们自定义的处理state数据的方法。
2.useReducer的使用
以计数器为例
引入useReducer
import { useReducer } from "react"
创建reducer函数,传入参数state、action
通过action的type属性确定执行不同的函数体
function reducer(state,action){
switch(action.type){
case 'increment':{
return {
...state,
num:state.num + action.num
}
}
case 'decrement':{
return {
...state,
num:state.num - action.num
}
}
default:{
return state
}
}
}
通过dispatch函数传入action对象:{type:'xxx', data:xxx}
function App() {
const [state, dispatch] = useReducer(reducer, {num:1})
return (
<>
<div>{state.num}</div>
<button onClick={() => { dispatch({ type: 'increment',num:5})}}>+5</button>
<button onClick={() => { dispatch({ type: 'decrement', num: 5 }) }}>-5</button>
</>
)
}
3.useReducer的踩雷点
(1)使用dispatch()后发现state打印出来没有立即更新
state和useState的效果一样,是异步更新的,需要等到下一次dom改变才会更新,解决办法就是在dom中使用到state的数据
(2)在reducer里没有返回新的对象或数组会导致react跳过这次dispatch
React 使用 Object.is 比较更新前后的 state,如果 它们相等就会跳过这次更新。
// 直接更改对象或数组
case 'add':{
state.name = 'csq'
return state
}
Object.is 运作如下:
因此修改为:
case 'incremented_age': {
// 创建一个新的对象
return {
...state,
name: 'csq'
};
(3)reducer 和初始化函数运行了两次
严格模式 下 React 会调用两次 reducer 和初始化函数,但是这不应该会破坏代码逻辑。
这个 仅限于开发模式 的行为可以帮助你 保持组件纯粹:React 会使用其中一次调用结果并忽略另一个结果。如果你的组件、初始化函数以及 reducer 函数都是纯函数,这并不会影响你的逻辑。不过一旦它们存在副作用,这个额外的行为就可以帮助你发现它。
(二)useContext
1. 什么是useContext?
useContext是一个用于读取和订阅组件中的context的 Hook
当组件嵌套过深又需要传递数据时,一层一层的传props是不现实的,Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。
这样就需要useContext,在顶层组件创建context,将context标签包裹住需要传值的子组件,这样嵌套在里面的组件就可以获取到这个context的值了
2.useContext的使用
以点击复选框改变form和button的主题色为例:
引入createContext、useContext,并创建主题context,传入初始值dark(可以为空)
import { createContext, useContext} from "react"
const ThemeContext = createContext('dark')
建立组件,在顶部组件设置theme响应式,点击复选框改变theme的值
function App() {
const [theme, setTheme] = useState('dark')
return (
<div className="main">
<Form></Form>
<label>
<input
type="checkbox"
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}s
/>
Use dark mode
</label>
</div>
)
}
function Form() {
return (
<div className='form'>
{/* */}
<Panel>
<Button value={'登录'} ></Button>
<Button value={'登出'} ></Button>
</Panel>
</div>
)
}
function Panel({ children }) {
return (
<>
<div className="panel">
<h1>Welcome</h1>
{children}
</div>
</>
)
}
function Button({ value }) {
return (
<>
<button>{value}</button>
</>
)
}
将顶部组件的theme传递给form组件和button组件:
用<ThemeContext.Provider>标签包裹需要获取context的组件,value={theme}传值给ThemeContext,需要的子组件再提供useContext()获取
function App() {
const [theme, setTheme] = useState('dark')
return (
<div className="main">
<ThemeContext.Provider value={theme}>
<Form></Form>
<label>
<input
type="checkbox"
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}s
/>
Use dark mode
</label>
</ThemeContext.Provider>
</div>
)
}
function Form() {
const theme = useContext(ThemeContext)
return (
<div className={'form '+ theme}>
{/* */}
<Panel>
<Button value={'登录'} ></Button>
<Button value={'登出'} ></Button>
</Panel>
</div>
)
}
...
function Button({ value }) {
const theme = useContext(ThemeContext)
return (
<>
<button className={theme} >{value}</button>
</>
)
}
这样就实现点击复选框改变主题颜色了!
3.useContext的踩坑点
(1)当使用多个context.provider标签包裹子组件时
子组件的useContext()会选取离他最近的provider的值
<ThemeContext.Provider value={1}>
...
<ThemeContext.Provider value={2}>
<Button></Button> // useContext获取到的是 2
</ThemeContext.Provider>
</ThemeContext.Provider>
(2)当没有使用 context.provider标签时
useContext()获取到的值就是最开始createContext()的初始值
(3)在传递对象和函数时优化渲染
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
每当
MyApp
出现重新渲染(例如,路由更新)时,这里将会是一个 不同的 对象指向 不同的 函数,因此 React 还必须重新渲染树中调用useContext(AuthContext)
的所有组件。如果基础数据如
currentUser
没有更改,则不需要重新渲染它们。为了帮助 React 利用这一点,你可以使用 useCallback 包装login
函数,并将对象创建包装到 useMemo 中。
useCallBack和useMemo这两个钩子目前还没有学到,先挖个坑~~~
(三)Redux
介绍:redux是一个用于集中管理状态的库,类似于vue的store
redux原理跟reducer相同,dispatch派发action来更新,action是一个普通的JavaScript对象,用来描述这次更新的type和content。
1.redux的使用
下载react-redux:
npm install redux --save
不知道为啥我下面写的那些在组件里没有实现响应式。。。懵逼
哦是因为没有在组件里的mount时钟周期里订阅。。先放在这里
(1)创建store
和useReducer一样,创建初始state对象、创建reducer,再初始化store,使用store.subscribe订阅state的变化
目前redux的createStore方法已经被redux/toolkit取代,后面会提及
import { createStore } from "redux";
// 初始state
const initialState = {
num: 0
}
// 创建reducer 必须是纯函数 不能在里面直接修改state
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'increment': {
return {
...state,
num: state.num + action.num
}
}
case 'decrement': {
return {
...state,
num: state.num - action.num
}
}
default: {
return state
}
}
}
// createStore的写法已被取代
const store = createStore(reducer)
// 订阅state的变化
store.subscribe(() => {
console.log("state发生了改变....", store.getState().num);
});
export default store
(2)在组件中使用
使用store.dispatch()传入action对象
使用store.getStore()获取state对象数据
import store from "./store"
function App() {
return (
<>
<div>{store.getState().num}</div>
<button onClick={() => { store.dispatch({ type: 'increment', num: 5 }) }}>+5</button>
<button onClick={() => { store.dispatch({ type: 'decrement', num: 5 }) }}>-5</button>
</>
)
}
(3)拆分组件
一般会将上面的部分拆分为四个文件:
- index.js 入口文件
- reducer.js reducer函数
- actionCeactors.js 存放action函数
- constant,js 存放type常量
// index.js
import { createStore } from "redux";
import reducer from './reducer.js'
// createStore的写法已被取代
const store = createStore(reducer)
// 订阅state的变化
store.subscribe(() => {
console.log("state发生了改变....", store.getState().num);
});
export default store
// reducer.js
import { INCREMENT, DECREMENT } from "./constant";
const initialState = {
num:0
}
const reducer = (state = initialState, action) => {
console.log('state', state, 'action', action);
switch (action.type) {
case INCREMENT: {
return {
...state,
num: state.num + action.num
}
}
case DECREMENT: {
return {
...state,
num: state.num - action.num
}
}
default: {
return state
}
}
}
export default reducer
// actionCreator.js
import { INCREMENT, DECREMENT } from "./constant";
export const incrementAction = (num)=>{
return {
type: INCREMENT,
num
}
}
export const decrementAction = (num)=>{
return {
type: DECREMENT,
num
}
}
// constant.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
在组件里使用:
import store from "./store"
import { incrementAction, decrementAction } from "./store/actionCreators"
function App() {
return (
<>
<div>{store.getState().num}</div>
<button onClick={() => { store.dispatch(incrementAction(5)) }}>+5</button>
<button onClick={() => { store.dispatch(decrementAction(5)) }}>-5</button>
</>
)
}
2.reduxjs/toolkit的使用
在react开发中,默认toolkit和react-redux一起使用
将 Redux Toolkit 和 React Redux 包添加到项目中
npm install @reduxjs/toolkit react-redux
还是以计数器为例:
(1)创建store
在src/app/store.js文件中创建store
import { configureStore } from "@reduxjs/toolkit";
export default configureStore({
reducer:{
},
})
在main.jsx中:
使用react-redux的Provider组件包裹整个App组件,将store以prop形式传递
这里就有useContext那味儿了
import store from './app/store.js'
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)
(2)创建reducer
在src/features/counterSlice.js中创建计数器组件的reducer
可以把counterSlice理解为counter组件store的切片,在里面把不同的action进行切片处理
通过createSlice()创建counter组件的reducer
// 存放counter组件的reducer
import { createSlice } from "@reduxjs/toolkit";
// 初始化数据
const initialState = {
num:0
}
export const counterSlice = createSlice({
name:'counter',
initialState:initialState,
reducers:{
increment:(state, action)=>{
console.log(action);
// createSlice API内置了Immer,因此可以直接操作state
state.num = state.num + action.payload
},
decrement:(state, action)=>{
state.num = state.num - action.payload
},
}
})
console.log(counterSlice.actions);
// 获取createSlice()的action
export const { increment, decrement } = counterSlice.actions
// redux使用createReducer()创建了这个reducer
export default counterSlice.reducer
注意:createSlice API内置了Immer,因此在reducers里可以直接修改state
reducer里函数的两个形参分别是state、action,action分为type和payload
createSlice()返回的实际是一个对象:
counterSlice.actions:
counterSlice.reducer是一个函数,由buildReducer方法创建
再把buildReducer掏出来就会发现,redux的reducer就是用createReducer创建的
再掏。。有点看不懂了,但是都是蛮眼熟的代码,额算了
(3)将reducer引入到store中,在组件中使用
// 将counterReducer添加到store里
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
reducer:{
counter: counterReducer,
},
})
在组件里使用:
通过引入useSelector()获取state数据,useDispatch()来获取dispatch函数,再使用在counterSlice导出的action传参,传入的数据就会变为reducers的函数里的action.payload
import { useSelector, useDispatch} from 'react-redux'
import { increment,decrement } from './features/counter/counterSlice'
function App() {
const num = useSelector((state)=>state.counter.num)
const dispatch = useDispatch()
return (
<>
<div>{num}</div>
<button onClick={() => { dispatch(increment(5)) }}>+5</button>
<button onClick={() => { dispatch(decrement(5)) }}>-5</button>
</>
)
}
这样就能实现state响应式啦,还不需要在每个组件里订阅,蛮方便蛮方便
(四)总结
本篇文章介绍了useReducer,useContext以及rudux的简单应用
- useReducer Hook,用于集中管理state,可以说是小型的redux,但是只能在组件内使用;
- useContext Hook,可以实现爷孙组件的传递,无论有多深;
- redux,适配react的状态管理库,原来的写法过于复杂,toolkit和react-redux结合的写法更简单一些,但是由于我对react的组件化结构还不太了解,目前只能算是初步学习到了redux;
学习的时候一直纠结useReducer和redux的区别在哪,经查阅后发现可以用useReducer和useContext写一个简单的redux,下一篇就写这个,来更好的体会redux和react的特性