This article is not a best practice guide for hooks, nor is it a State Manager like HOX, but rather looks at how to abstract and encapsulate component logic when developing a business using React Hooks.
React releases Hooks: “Hooks fix a lot of previous development issues”
We have encountered over five years of writing. Hooks solve a wide variety of unconnected problems in React that we’ve encountered over five years of writing and maintaining tens of thousands of components.
hard to reuse stateful logic between components complex components become hard to understand * class confuse both people and machines
Hooks avoid reuse of class Component state logic by writing it “functionally”. However, the functional writing method is too flexible, which also brings some challenges to our business logic abstraction and assembly. Next, this article discusses the theory and practice of React Hooks in business development around several major issues.
Hooks can return UI components
My answer: try to avoid it.
Hooks are completely function, which can return any element, state, component, or something else. But if custom hooks return a function Component, the function Component references will change when state is switched inside. Rerender of the outer component is raised (because the reference value of useCostomHooks changed). This raises two problems:
- Performance issues (although they may not be obvious in most cases)
- When the state switch has a transition animation effect, the “flicker” is visible because defaultValue is different from props. Value. Like MyModal in the example below.
Example: useModal codesandbox. IO/s/cool – yalo…
/** * const { MyButton, MyModal } = useModal(); * * * < div > < MyButton > edit < / MyButton > * < MyModal onOk = {() = > {}} > popup window content < / MyModal > * < / div > * / const useModal = () = > { const [on, setOn] = useState(false); const toggle = () => setOn(! on); const MyBtn = props => <Button {... props} onClick={toggle} />; const MyModal = ({ onOk, ... props }) => ( <Modal {... props} visible={on} onOk={async () => { onOk && (await onOk()); toggle(); }} onCancel={toggle} /> ); return { MyBtn, MyModal }; };Copy the code
The useModal method internally encapsulates the state of controlling Modal display implicit, and returns two UI components, MyModal and MyButton.
When the Modal onOk callback is triggered, the visible state is switched, and rerender of useModal returns a new MyModal reference and causes rerender of the outer component. Performance is slightly affected, but the UI doesn’t show any problems. If you add a state inside useModal, such as confirmLoading, the “flicker” problem becomes obvious:
onOk={async () => {
if (onOk) {
setConfirmLoading(true);
await onOk();
setConfirmLoading(false);
}
toggle();
}}Copy the code
The onOk call followed by the confirmLoading state switch causes the useModal (and the outer component) to rerender one more time: In rerender, Modal is remounted, default Visible is false, but the passed Visible is true, so it causes Modal to animate from closed to displayed. This is the “flicker problem” mentioned earlier: Modal appears to close and open before the popover actually closes.
UseModal rerender 1 times after adding confirmLoading
When okOk operates asynchronously,setConfirmLoading(true)
Rerender1 (one rerender brought by confirmLoading)After onOk is executed,
setConfirmLoading(false)The state update with toggle() is merged to rerender 1 (which is already there)
In summary, use the hooks principle: abstract only the data logic, not the UI components. Component encapsulation is left to componentization.
UseEffect: How to compare object type deps changes
In a Function Component, it is common to define a variable of type OBEjct (such as queryObj) and perform some side effects when it changes. UseEffect detects dePS changes through shallow comparisons, which results in queryObj being found every time you rerender! == queryObj to execute effect multiple times. So how can you tell if a dependency of type object has changed?
- You can use json.stringify to change an object to string as a dependency;
- Implement a Deep Comparable useEffect: Return the DEPS of useEffect as memoized Value.
import { useEffect, useRef } from 'react'; import { isEqual, cloneDeep } from 'lodash'; const useDeepCompare = value => { const ref = useRef(); if (! isEqual(value, ref.current)) { ref.current = cloneDeep(value); } return ref.current; }; const useDeepEffect = (callback, deps) => { useEffect(callback, useDeepCompare(deps)); }; export const useDeepUpdate = (callback, deps) => { const didMountRef = useRef(null); useDeepEffect(() => { if (! didMountRef.current) { didMountRef.current = true; return; } callback(); }, deps); }; export default useDeepEffect;Copy the code
Take a list page as an example to talk about composition and abstraction of data
The list page is usually divided into two parts: Filter and Table (display list + page turning).
If state is defined at a finer granularity, component state might be defined like this:
const [filter, setFilter] = useState({});
const [pagination, setPagination] = useState({
current: 1,
pageSize: PAGE_SIZE,
total: 1,
});
const [data, setData] = useState([]);
const { current, pageSize } = pagination;Copy the code
Then, for side effects, the behavior is slightly different when filter and current change: the current page needs to be reset when the “query” button is clicked in the filter section. So just pass a parameter to the fetchData method that says reset currentPage, okay?
UseDeepEffect (() => {fetchData(true); }, [filter]); UseEffect () => {fetchData(false); useEffect() => {fetchData(false); }, [current]);Copy the code
And then fetchData quickly writes:
Const fetchData = async resetPage => {const fetchData = async resetPage; if (resetPage) { page = 1; } const res = axios.post('/xxx', { ... filter, page, pageSize }); const { data = [], total = 0 } = res.data; const newPagination = { ... pagination, total }; if (resetPage) { newPagination.current = 1; } setData(data); setPagination(newPagination); };Copy the code
There is also behavior differentiation in fetchData based on the value of the resetPage, and while the code looks disgusting, the logic doesn’t seem complicated either. However, there seems to be something wrong: if resetPage is true, setPagination changes current, and then useEffect… FetchData is called again, right?
The problem is that the two DEPs of useEffect, current and filter, are dependent: the change of filter will lead to the change of current. In other words, for network requests, the combination of filter with Page and pageSize is the dependency that causes effect.
In other words, from the perspective of data flow, Filter+pagination is a kind of Filter, both of which are generalized filters. What’s left is a list of presentation types. Reset state:
const { filters, setFilters } = useState({ current: 1 }); Const [data, setData] = useState([]); // Put all parameters related to the network request into filters const [total, setTotal] = useState(0); const { current } = filters; const fetchData = () => { const res = axios.post("/xxx", { ... filters, pageSize: PAGE_SIZE }); const { data, total } = res.data; setData(data); setTotal(total); }; useDeepEffect(() => { fetchData(); }, [filters]); onFilterChange = params => { setFilters({ ... filters, ... params, current: 1 }); }; // Remember to reset current Page onPagination = Page => {setFilters({... filters, current: page }); };Copy the code
At this point, the data flows work, and with some refinement, you can wrap these as Custom Hooks: useListData as a unified data flow management approach for list pages.
Above, what we’re really talking about is “UI oriented” versus “request oriented” state definition. In the case of hooks operations, the complexity often comes from managing side effects, request-oriented state definitions, and the ability to encapsulate dependencies directly into a “cage”. At least for the list page scenario, the data processing logic is much simpler and clearer by defining state this way.
UseListData in multi-tab scenarios
There are two types of variables exported from the useListData method
- State: loading, dataSource and pagination
- Function: setFilter, setPagination, updateListData, refreshListData
This method wrapper clearly meets the usual filter panel + list page requirements. If you change to filter panel + multi-tab list, does useListData still meet the requirements? 🧐
With the useEffect “auto run” method, nesting layers is not a problem. Since useListData is a data request that is triggered by changes in the filters, the above UI changes do not affect the internal logic of useListData. We just need to: Pass the queryObj of the top filter as props into the TabPane, and define a side effect in the TabPane.
const { setFilter } = useListData('/api/xxx', params);
useDeepUpdate(() => {
setFilters(queryObj);
}, [queryObj])Copy the code
So far, you can see that logic abstracted by hooks makes it easy to reuse logic in similar scenarios. Moreover, it also effectively reduces the amount of code for each file, which is convenient for subsequent maintenance. This scenario also echoes the principle originally proposed: hooks are best used to encapsulate only data logic, not to return UI components. This way, our useListData can still be used after the UI changes.
Hooks && Hoc: Have it too
Hoc (High Order Component) was a common operation to reuse class Component logic before hooks came along. Hooks allow logic reuse at low cost and avoid layer upon layer nesting of components. Arguably, in the age of hooks, Hoc will be used less and less. While using hooks, I found one scenario that was particularly good with Hoc.
Only call Hooks at the top level. Because React needs to rely on the order of hooks calls to determine which useState. So require that hooks be called in the same order every render.
In some scenarios, we need to check whether some data meets the criteria first, and return null or Empty if not. At this point, encapsulation of the checking logic into Hoc is perfectly appropriate, because strictly speaking, this checking method is not part of the business logic in the Function Componen. Also, encapsulating the checking logic into Hoc can avoid the “run n” problem that ESLint often presents with false “run n” warnings. (The feeling is that ESLint is being too strict, and doing so at the top of the file doesn’t lead to hooks run n, because if the conditions aren’t met, Hooks will not be implemented at all.
So you can abstract a generic Hoc, such as checkRequiredDataHoc
const checkRequirdDataHoc = ( checkFunc, placeholderElement, ) => WrappedFunction => props => { if (typeof checkFunc === 'function' && checkFunc(props)) { return <WrappedFunction {... props} />; } return placeholderElement || null; };Copy the code
conclusion
Hooks make it possible to reuse business logic in some common scenarios, and are flexible enough to be used in many similar scenarios if the principle is to abstract only the data logic. With the popularity of hooks, various custom hooks need to be precipitated within the team. Should it be a component library or a function utility library, or something else?