使用 React
的时候,比较常见的状态管理工具就是 Redux
,虽然 Redux
写的代码特别多,但是不可否认的是 Redux
的设计原则中的数据不可变这条原则,其实完美的契合了 React
。因而在 Redux
中广泛使用,再加上丰富的中间件,比如 redux-thunk
,redux-saga
,Dva
等,Redux
的热度一直很高,在 Hook
时代,也有 useSelector
useDispatch
这些很有用的 Hook
,大大提高开发效率。不过 Hook
时代,也可以通过 useReducer
Context API
实现类似 Redux
,这样就不用导入 Redux
了
React.createContext
官方文档,官网文档如是说,其实就是一个父组件的状态,全部子组件都能获取,并且不需要通过属性一层一层传递
React.useReducer
useReducer,useReducer
的用法和 Redux
的用法非常相似,需要传入一个 reducer
和初始的 state
,返回一个 state
和一个类似 Redux dispatch
的操作函数,同时也符合 Redux
的设计理念,数据只读,通过纯函数修改数据。
Context 和 useReducer 结合
可以通过将 useReducer
返回的 state
和 dispatch
作为 Context.Provider
的 value
,这样被 Provider
包裹的子组件就都能获得 state
和操作 state
了
// store.js
import { createContext, useReducer } from 'react';
const initState = {
count: 0,
text: '',
};
const reducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'add':
return { ...state, count: state.count + 1 };
case 'minus':
return { ...state, count: state.count - 1 };
case 'change-text':
return { ...state, ...payload };
default:
return state;
}
};
export const Store = createContext(null);
const Provider = (props) => {
const [state, dispatch] = useReducer(reducer, initState);
return (
<Store.Provider value={{ state, dispatch }}>
{props.children}
</Store.Provider>
);
};
export default Provider;
在 store.js
里面初始化了一个 Store
,初始化的时候默认可以不传值,可以通过 Store.Provider
传值,通过 useReducer
初始化 state
和 dispatch
,这里可以看到 useReducer
的入参和 Redux
一样,都是默认的 state
和 reducer
如何使用,只要把这个组件包裹在你需要这些状态的地方就好了,如果是全局的状态,直接包裹在最外面的组件就好
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import Provider from './store';
ReactDOM.render(
<Provider>
<App />
</Provider>,
document.getElementById('root')
);
这样包裹的话,整个 React
应用都能使用 state
和 dispatch
了
子组件如何使用
上面提到怎么创建一个 Provider
和怎么注入到组件中,组件怎么使用这个状态依旧没有提到,实现方法主要有两个。
- 可以借用
useContext
- 可以参考一下
React-Redux
的做法,用高阶组件connect
包裹获需要获取状态的组件
useContext
const value = useContext(MyContext);
官方文档,useContext
可以订阅 Context
对象,当 Context
对象的 Provider
的 value
发生改变的时候,useContext
会获取最新的最新的 value
假设应用根组件就是 App.js
,Count
和 Txt
分别依赖 state.text
和 state.count
// app.js
import Count from './count';
import Txt from './txt';
function App() {
return (
<div>
<Count />
<Txt />
</div>
);
}
export default App;
// count.js
import { useContext } from 'react';
import { Store } from './store';
const Count = () => {
const { dispatch, state } = useContext(Store);
const add = () => {
dispatch({ type: 'add' });
};
const minus = () => {
dispatch({ type: 'minus' });
};
return (
<div>
<div className="count">{state.count}</div>
<button onClick={add}>Add</button>
<button onClick={minus}>Minus</button>
</div>
);
};
export default Count;
import { useContext } from 'react';
import { Store } from './store';
const Txt = () => {
const { dispatch, state } = useContext(Store);
const changeText = () => {
const text = Math.random().toString();
dispatch({ type: 'change-text', payload: { text } });
};
return (
<div>
<div className="txt">Text: {state.text}</div>
<button onClick={changeText}>Change Text</button>
</div>
);
};
export default Txt;
按照上面的写法,Count
和 Txt
都能获得到 state
并且还能操作 state
了,但是这样有一个问题,仔细看官网文档,只要 value
发生改变了就会重新渲染,这里明显有问题的,Count
只依赖 state.count
, Txt
只依赖 state.text
,但是上面那个写法,摆明就就是 state
改变了两个组件就会重新渲染,解决方案也挺多的
- 最简单的避免方式,在
App
中取这个变量,然后分发到不同的组件中去,例如
import Count from './count';
import Txt from './txt';
import { Store } from './store';
function App() {
const { dispatch, state } = useContext(Store);
return (
<div>
<Count count={state.count} dispatch={dispatch} />
<Txt text={state.text} dispatch={dispatch} />
</div>
);
}
export default App;
- 可以借助
useMemo
useMemo useCallback
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
官方链接,useMemo
是一个记忆化的值,当它的依赖改变的时候,它才会重新执行函数,这个和 useCallback
差不多,useCallback
是返回一个记忆化的函数,当依赖发生改变的时候,才会重新生成一个新的函数
这个看起来和上面的内容没什么关联,但是仔细想一下,组件每次执行也是返回一个 jsx
组件,如果只要必要依赖的值发生改变的时候,才返回新的组件,不然就返回一个记忆化的组件
配合 useMemo
, Txt
组件可以修改成这样,dispatch
是一定不会改变的,所以 useCallback
也可以把依赖写成空数组,如果不用 useCallback
,每次 state
改变的时候,changeText
就重新定义了,这样还是会导致 useMemo
重新执行,或者可以不把 changeText
加入 useMemo
依赖列表,在这个组件是可以的,最好还是建议写上去,Count
组件也是同样的修改
const Txt = () => {
const { dispatch, state } = useContext(Store);
const changeText = useCallback(() => {
const text = Math.random().toString();
dispatch({ type: 'change-text', payload: { text } });
}, [dispatch]);
return useMemo(
() => (
<div>
<div className="txt">Text: {state.text}</div>
<button onClick={changeText}>Change Text</button>
</div>
),
[state.text, changeText]
);
};
实现一个 connect
使用 useContext
有个缺点,就是使用到 Store
的地方,必须把源 Store
导进来,写多了也很繁琐。解决的方式也是有的
- 可以参考
react redux
的connect
高阶组件,实现一个connect
如下
import { useContext, useMemo } from 'react';
import { Store } from './store';
const connect = (fn) => {
if (typeof fn !== 'function') {
throw new Error('first param must be function');
}
return (WrappedComponent) => (props) => {
const { dispatch, state } = useContext(Store);
const value = fn(state);
return useMemo(
() => <WrappedComponent {...props} {...value} dispatch={dispatch} />,
// 注意这里
[JSON.stringify(value), dispatch, props]
);
};
};
export default connect;
仔细看下,这里就是一个很简单的高阶组件,没有实现 connect
的 equalFn
第二个参数,第一个参数和 connect
一样,都是必须返回一个对象.因为是返回一个对象,所以,每次执行函数的返回也是不一样的,value
的值只要 state
做了改变就会改变,就像刚刚的 Text
和 Count
组件一样,修改了一个和组件内部无任何关联的属性,组件也重新渲染了,这样不符合要求。这里使用一个简单的 JSON.stringify
处理。Redux connect
的处理方式是实现一个 shallowEqual
,实现起来也很简单
- 使用
connect
import connect from './connect';
const Count = ({ num, dispatch, count }) => {
const add = () => {
dispatch({ type: 'add' });
};
const minus = () => {
dispatch({ type: 'minus' });
};
console.log('wrapper reload');
return (
<div>
<div className="count">count from context {count}</div>
<div className="num">num from props {num}</div>
<button onClick={add}>Add</button>
<button onClick={minus}>Minus</button>
</div>
);
};
export default connect((state) => ({
count: state.count,
}))(Count);
这样能解决问题,但是 Hook
时代也有 Hook
的解法,可以参考 react-redux
的 useDispatch
useSelector
,但是笔者太菜,折腾了半天依旧没有写出像 react-redux useSelector
那让无关属性不会导致组件重新渲染的,所以使用的时候还得搭配 useMemo
useSelector useDispatch useMemo
// useDispatch useReducer 生成的 dispatch 是绝对不会改变的,所以直接返回就好
const useDispatch = () => {
const { dispatch } = useContext(Store);
return dispatch;
};
// 折腾了半天,也没有找到解决方法,干脆放飞自我了
const useSelector = (fn) => {
if (typeof fn !== 'function') {
throw new Error('first param must be function');
}
const { state } = useContext(Store);
const value = fn(state);
return value;
};
// 使用的时候要调配 useMemo,这样也方便一些,不用每个组件都 useContext 一下
const Count = () => {
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
const add = useCallback(() => {
dispatch({ type: 'add' });
}, [dispatch]);
const minus = useCallback(() => {
dispatch({ type: 'minus' });
}, [dispatch]);
return useMemo(() => {
console.log(count);
return (
<div>
<div className="count">{count}</div>
<button onClick={add}>Add</button>
<button onClick={minus}>Minus</button>
</div>
);
}, [add, count, minus]);
};
总结
Hook
出来之后,React
的可玩性就更高了,一个简单的状态管理,还能玩出花来。希望这篇文章给读者带来收获,还有,新年快乐!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!