"It's been 1202, why are people still using Redux"-this is probably the first reaction of many people seeing this article. First, let me make it clear that this article does not discuss whether or not Redux should be used. This is a relatively large topic and should be a separate article. And there are already a lot of discussions in the community, you can always find some comparison diagrams of advantages and disadvantages from several highly praised articles, and then make a final decision based on the scene of your project. Let's just cite a few reasons why teams use Redux. First of all, it is easy to understand. Redux has been complained a lot about because of the cumbersome writing, but behind the tedious writing, there are not so many black technologies, and it is very easy to troubleshoot problems. In addition, Redux essentially puts forward a standard paradigm for logical processing methods, and is matched with a set of practice specifications, which helps to maintain the consistency of project code writing style and organization method. This is especially true in projects developed by multi-person cooperation. important. Other advantages will not be repeated here.
At this time, some students may want to ask, if you talk about Redux, what does it have to do with hooks? As we all know, shortly after the React team launched the concept of Hooks, Redux also updated the corresponding API to support it. The essence of Hooks is the encapsulation of logic and the decoupling of logic and UI code. With the blessing of Hooks, our Redux React project can be more concise, easy to understand, and more extensible. And the Hooks API is currently the highly recommended level of Level 2 in Redux's best practice recommendations. It has a more concise way of expression, a cleaner number of React nodes, and a more friendly typescript support.
How to use specific Redux-related APIs is not introduced here, you can directly jump to the official documentation to understand. Below we will specifically talk about an application scenario, how they help us better organize the code. Some of the engineering-level code comes from the project template of react-boilerplate, which provides a lot of help in dynamic loading issues.
Package case
When developing large-scale React applications, dynamic lazy loading of code is always a must in our project architecture. The code splitting, dynamic quoting, etc., engineering tools have already done it for us. What we need to pay more attention to is what additional operations are required for dynamic introduction and unmounting, and how to expose this work to project developers as little as possible. As mentioned earlier, Hooks' most powerful ability lies in the encapsulation of logic, and of course his power must be used here.
Here we take Reducer as an example, other middleware, such as Saga, etc. can be analogized, if necessary, you can post the corresponding code later. We divide the entire package into three layers: core implementation, composable package, and exposed package to developers. Below we explain them one by one in order. (In the specific implementation, I will bring the implementation of the connected router by default, so that you can use it directly if you need to copy the code)
Core realization
The code here implements the logic of how to mount and unmount the split Reducers for a store.
// 本段代码完全来自于 react-boilerplate 项目
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';
import history from '@/utils/history';
import checkStore from './checkStore'; // 做类型安全检测的,不用关心
function createReducer(injectedReducers = {}) {
return history => combineReducers({
router: connectRouter(history),
...injectedReducers,
});
}
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(src/utils...) injectReducer: Expected `reducer` to be a reducer function',
);
if (
Reflect.has(store.injectedReducers, key)
&& store.injectedReducers[key] === reducer
) return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers)(history));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}
There is a special point in this paragraph that needs to be talked about. You may find that there is no unmounting part at all. This is because the reducer is special, it does not produce side effects, and because the current method provided is to mount the new reducer through the entire replacement method, there is no need to unmount it separately. When dealing with the mounting of other middleware, especially those with side effects (such as redux-saga), we need to implement an eject method to unmount accordingly.
OK, then we can now provide an injectReducer with the ability to inject a Reducer for the entire project through the getInjectors method (may also include an eject method). The next step is how to schedule this capability.
Composable package
Here, we hope that through a custom hooks, developers can declare a reducer of a certain namespace for a component to mount and unmount in accordance with its life cycle. Developers only need to pass in the reducer namespace and reducer implementation, and put this hooks into the corresponding component logic.
import React from 'react';
import { ReactReduxContext } from 'react-redux';
// 这是我们在上一步实现的 injector 工厂,通过他来产出一个与固定 store 绑定的 injectReducer 函数
import getInjectors from './reducerInjectors';
const useInjectReducer = ({ key, reducer }) => {
// 需要从 Redux 的 context 中获取到当前应用的全局 store 实例
const context = React.useContext(ReactReduxContext);
// 为了模拟 constructor 的运行时机
const initFlagRef = React.useRef(false);
if (!initFlagRef.current) {
initFlagRef.current = true;
getInjectors(context.store).injectReducer(key, reducer);
}
// 如果需要加入 eject 的逻辑,则可以使用这样的写法。类似于为当前组件增加一个 willUnmount 的生命周期逻辑。
// React.useEffect(() => (() => {
// const injectors = getInjectors(context.store);
// injectors.ejectReducer(key);
// }), []);
};
export { useInjectReducer };
The useInjectReducer hooks help us deal with when to mount, how to mount and other issues, we only need to tell him what to mount in the end. Through this layer of encapsulation, we can find that we have further converged our concerns. Up to this point, we have provided a project-level public method. In the next step, we will provide a unified writing method to use in the specific development process to further encapsulate and converge.
Before entering the next step, let's briefly explain the above logic. The logic is divided into three paragraphs by comments (the third paragraph is not used in the reducer scenario). In the first paragraph, we get the store reference through the redux context where the current component is located. In the second and third paragraphs, we make the components separately Perform mounting and unmounting operations before initialization and destruction. Simulate the life cycle of the constructor through a component with initFlagRef as functional (if you have a better implementation, please advise), because if you inject after mounting, you will not get the content of the corresponding store in the first rendering.
Expose the package to developers
After completing the encapsulation of the public method, our next step is to consider how to mount the store for our module in a simpler way. In the following way, the developer does not need to care about anything, and can complete the mounting in one sentence without providing additional parameters. If there are reducer, saga or other middleware content at the same time, they can also be packaged together.
import {
useInjectReducer,
// useInjectSaga,
} from '@/utils/store';
import actions from './actions';
import constants from './constants';
import reducer from './reducer';
// import saga from './saga';
const namespace = constants.namespace;
const useSubStore = () => {
useInjectReducer({ key: namespace, reducer });
// useInjectSaga({ key: namespace, saga });
};
export {
namespace,
actions,
constants,
useSubStore,
};
Practical use example:
import React from 'react';
import {
useSubStore,
} from './store';
export default function Page() {
useSubStore();
return <div />;
};
The specific data and logic can also be encapsulated into several Hooks. For example, we need to provide an array of data for simple operations. We only care about the addition and the quantity, so we can encapsulate one Hooks, so that the actual user only needs to care about the addition and the quantity. Elements, don't care about the specific implementation of redux.
import { useMemo, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
actions, constants, namespace,
} from './store';
export function useItemList() {
const dispatch = useDispatch();
const list = useSelector(state => state[namespace].itemList);
// 这只是范例!
const count = useMemo(() => list.length, [list]);
const add = useCallback((item) => dispatch(actions.addItem(item)), []);
return [count, add];
}
Let's modify the place used below:
import React from 'react';
import {
useSubStore,
} from './store';
import { useItemList } from './useItemList';
export default function Page() {
useSubStore();
const [count, add] = useItemList();
return <div onClick={() => add({})}>{count}</div>;
};
Through such a split method, the definition of the store, and the logic of the use of the store, the business side only pays attention to the parts that they must pay attention to, and the changes of any party can cause as few changes as possible.
Reusable Hooks
Let's think about it further. In the past, we might have a page corresponding to a store. After splitting through Hooks, it is more convenient for us to split the store from the functional level, and the logic of the store will be clearer. After the interaction with the store is encapsulated into Hooks, it can be used in multiple display layers quickly. This will show great value in the complex B-side workbench scene. The case will be a bit long, and I can add it later when I have time.
review
After reading the above example, I believe that smart readers already know the problem I want to express. By combining Redux + Hooks, the definition code is standardized, and the logic, call, and definition are decoupled to a certain extent. Through the simplified API, the cost of logical understanding is reduced, the complexity of subsequent maintenance is reduced, and reuse can be achieved to a certain extent. Whether it is compared to the past Redux access solution, or compared to simply using Hooks, it has its own unique advantages. It is especially suitable for workbench scenarios with relatively complex logic. (And I really like Saga's design ideas, it's cool to use).
OK, close. This time, a simple example is used to show a little about the chemical reaction of Redux with it in the environment of Hooks. The main thing I want to show is a design idea that relies on the logic encapsulation ability of Hooks. Redux black students should not be too entangled with this selection. Carrots and vegetables have their own loves.
I hope this series can continue to be written :)
Author: ES2049 / armslave00The article can be reprinted at will, but please keep this link to the original text.
You are very welcome to join ES2049 Studio if you are passionate. Please send your resume to caijun.hcj@alibaba-inc.com.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。