preface

“Design patterns” is a commonplace topic, but it is more focused on the object-oriented language domain, such as C++, Java, etc. Design patterns are not very popular in the front end, and many people feel that they are difficult to value in a typical process-oriented language like JavaScript. Previously, I held a similar view. My understanding of design patterns was only at the conceptual level, and I did not have a deep understanding of their practice in front-end engineering. Recently read the book JavaScript Design Patterns and Development Practices, which introduces 15 common design patterns and basic design principles, and how to implement them elegantly in JavaScript and apply them to real projects. As it happened, the team had recently held a debate on Hooks, whose core ideas are functional programming, and decided to explore whether design patterns would help us write more elegant Hooks.

Why design patterns

Protagonist in counter attack martial arts drama, ask the first master martial arts, the first old master water will only make the hero, basic skills, such as “ma bu” or horse stance just look, the protagonist is always complaining will all at this moment, but due to some objective reasons and had to insist, then start to learn the real skill when old master used before Epiphany, solid foundation after learning martial arts by leaps and bounds, Eventually become a generation of heroes. For us developers, “data structures” and “design patterns” are the basics that the old master taught us. They may not make us go faster, but they will certainly make us go farther and more steadily, helping us to write reliable and easy to maintain code that will avoid being “buried” in the future.

One of the criticisms that has been made since the Hooks were released is that maintenance costs have skyrocketed, especially for teams where there is a wide range of skill levels. Even if the entire framework of the project was built by an experienced student at the beginning, it will most likely become unrecognizable once it is given to a new employee to maintain for a period of time, let alone let the new employee use Hooks to develop zero-to-one projects. I understand that this is due to the high flexibility of the Hooks. Class Components have a set of lifecycle methods to restrict them. Only use hooks at the top level and only call hooks in React functions. On the other hand, while custom hooks improve the logic reuse rate of components, they also lead to the lack of design in abstraction by inexperienced developers. Logic abstractions in Class Component are often abstracted to pure functions, Hooks are encapsulated with useeffects, and bugs are expensive to fix.

So, since “design patterns” are fundamental, and “Hooks” are a new approach, let’s try to fix them from the perspective of design patterns.

What are the classic design patterns

Before we get into that, let’s take a quick look at some of the classic design patterns and principles we’ve forgotten. Every day, when we talk about design principles, we reduce them to “SOLID,” Corresponds to the Single Responsibility Principle, the Open Closed Principle, and the Liskov Substitution Principle Principle, Law of Demeter, Interface Segregation Principle and Dependence Inversion Principle. Design patterns also include singletons, policy patterns, broker patterns, iterator patterns, publish-and-subscribe patterns, command patterns, composite patterns, template method patterns, hang Yuan patterns, chain of Responsibility patterns, mediator patterns, decorator patterns, state patterns, adapter patterns, and so on.

There are a lot of great articles out there about the design principles and design patterns community, but I’m not going to go over them here, just to jog your memory.

1 plus 1 is greater than 2

Does it have to be useContext

In React Hook projects, when it comes to global state management, our intuition would be to use useContext. For example, suppose a project needs to decide whether to render certain components based on the information returned by the grayscale interface. Since the entire project shares a grayscale configuration, it is easy to think of it as a global state that can be obtained by calling the asynchronous interface at project initialization and initialized, and then obtained internally within the component using useContext.

// context.js
const GrayContext = React.createContext();
export default GrayContext;

// App.js
import GrayContext from './context';
function App() {
  console.log('App rerender');
	const [globalStatus, setGlobalStatus] = useState({});
	useEffect(() = > {
    console.log('Get GrayState');
		setTimeout(() = > {
			setGlobalStatus({
				gray: true
			});
		}, 1000); } []);return (
		<GrayContext.Provider value={globalStatus}>
    	<GrayComponent />
      <OtherChild />
    </GrayContext.Provider>
	);
}

// GrayComponent/index.js
function GrayComponent() {
  console.log('GrayComponent rerender');
  const grayState = useContext(GrayContext);

  return (
    <div>Child node {graystate.gray &&<div>Gray fields</div>}
    </div>
  );
}

// OtherChild/index.js
function OtherChild() {
  console.log('OtherChild rerender');
  return (
    <div>Other child nodes</div>
  );
}
Copy the code

However, the use of createContext causes all components in the Provider to be rerendered if the global state changes, even if it does not consume any information in the context.

If you think about it, this scenario is similar to the “publish and subscribe” pattern in the design pattern. We can define an instance of global state, GrayState, initialize the value in the App component, and subscribe to the changes in the instance in the child component to achieve the same effect. And only components that subscribe to the GrayState change will be rerendered.

// GrayState.js
class GrayState {
  constructor() {
    this.observers = [];
    this.status = {};
  }
  
  attach(func) {
    if (!this.observers.includes(func)) {
      this.observers.push(func); }}detach(func) {
    this.observers = this.observers.filter(observer= >observer ! == func); }updateStatus(val) {
    this.status = val;
    this.trigger();
  }

  trigger() {
    for (let i = 0; i < this.observers.length; i++) {
      this.observers[i](this.status); }}}export default new GrayState();

// App.js
import GrayState from './GrayState.js';
function App() {
  console.log('App rerender');
	useEffect(() = > {
    console.log('Get GrayState');
    setTimeout(() = > {
      const nextStatus = {
        gray: true}; GrayState.updateStatus(nextStatus); },200); } []);return (
		<div>
    	<GrayComponent />
      <OtherChild />
    </div>
	);
}

// GrayComponent/index.js
import GrayState from './GrayState.js'
function GrayComponent() {
  console.log('GrayComponent rerender');
  const [visible, setVisible] = useState(false);

  useEffect(() = > {
    const changeVisible = (status) = > {
      setVisible(status.gray);
    };
    GrayState.attach(changeVisible);
    return () = >{ GrayState.detach(changeVisible); }; } []);return (
    <div>Child node {visible &&<div>Gray fields</div>}
    </div>
  );
}
Copy the code

The result is the same, except that after the grayscale state is obtained, the GrayComponent that relies only on grayscale configuration information is rerendered.

For better reuse, we can also abstract the listener for Status into a custom Hook:

// useStatus.js
import { useState, useEffect } from 'react';
import GrayState from './GrayState';

function useGray(key) {
  const [hit, setHit] = useState(false);
  
  useEffect(() = > {
    const changeLocalStatus = (status) = > {
      setHit(status[key]);
    };
    GrayState.attach(changeLocalStatus);
    return () = >{ GrayState.detach(changeLocalStatus); }; } []);return hit;
}

export default useGray;

// GrayComponent/index.js
import useStatus from './useGray.js'
function GrayComponent() {
  console.log('GrayComponent rerender');
  const [visible, setVisible] = useGray('gray');

  return (
    <div>Child node {visible &&<div>Gray fields</div>}
    </div>
  );
}
Copy the code

Of course, it’s possible to rerender on demand with redux, but if you don’t have a lot of global state in your project, using redux is a bit of a stretch.

UseState or useReducer

Hooks beginners will often say, “I only used useState useEffect in my development, other Hooks don’t seem to be needed.” This feeling stems from an incomplete understanding of Hooks. UseCallback useMemo is a performance optimization hook that you only use when you need it. It’s possible to use it less often, but useReducer is worth paying attention to. In the official explanation, useReducer is an alternative to useState. Under what circumstances is it worth replacing? Here, an example is also used to analyze.

One of the most common examples of state mode is the order switcher in a music player.

function Mode() {
  /* Normal writing mode */
  const [mode, setMode] = useState('order');	// Define the mode state

  const changeHandle = useCallback((mode) = > {	// Mode switching behavior
    if (mode === 'order') {
      console.log('Switch to random mode');
      setMode('random');
    } else if (mode === 'random') {
      console.log('Switch to loop mode');
      setMode('loop');
    } else if (mode === 'loop') {
      console.log('Switch to sequential mode');
      setMode('order'); }} []);return (
    <div>
      <Button onClick={()= >ChangeHandle (mode)}> Switch mode</Button>
      <div>{mode.text}</div>
    </div>
  );
}
Copy the code

In the above implementation, you can see that the mode switch depends on the previous state, switching between the three modes “sequential play – random play – loop” in turn. Currently there are only three modes, and you can use the simple if… Else, but it can be difficult to maintain and extend once there are many patterns, so for this scenario where behavior is dependent on state, consider a “state pattern” redesign when branching grows to a certain extent.

function Mode() {
  /* Normal state pattern implementation */
  const [mode, setMode] = useState({});

  useEffect(() = > {
    const MODE_MAP = {
      order: {
        text: 'order'.press: () = > {
          console.log('Switch to random mode'); setMode(MODE_MAP.random); }},random: {
        text: 'random'.press: () = > {
          console.log('Switch to loop mode'); setMode(MODE_MAP.loop); }},loop: {
        text: 'loop'.press: () = > {
          console.log('Switch to sequential mode'); setMode(MODE_MAP.order); }}}; setMode(MODE_MAP.order); } []);return (
    <div>
      <Button onClick={()= >Mode.press ()}> Switch mode</Button>
      <div>{mode.text}</div>
    </div>
  );
}
Copy the code

According to the explanation of useReducer in React website, “In some scenarios, useReducer may be more applicable than useState, for example, the state logic is complex and contains multiple sub-values, or the next state depends on the previous state, etc.” Taking a look at the latter scenario here, the “state mode” of “class behavior is based on its state changes” is a typical scenario that depends on the previous state, which makes useReducer naturally suitable for multi-state switching business scenarios.

/* State mode can be easily implemented by using reducer */
const reducer = (state) = > {
  switch(state) {
    case 'order':
      console.log('Switch to random mode');
      return 'random';
    case 'random':
      console.log('Switch to loop mode');
      return 'loop';
    case 'loop':
      console.log('Switch to sequential mode');
      return 'order'; }};function Mode() {
  const [mode, dispatch] = useReducer(reducer, 'order');

  return (
    <div>
      <Button onClick={dispatch}>Switch mode</Button>
      <div>{mode.text}</div>
    </div>
  );
}
Copy the code

Custom Hook encapsulation principles

Custom Hook is an important reason for the popularity of React Hook. However, a custom Hook with poor abstraction may greatly increase the maintenance cost. The “The Hidden Side Effect” section of The Ugly Side of React Hooks enumerates The cost of troubleshooting for nested Side effects. It’s often said that design principles and design patterns help make code more maintainable and extensible, but what are some principles/patterns that help gracefully encapsulate custom Hooks?

OS: “How to gracefully encapsulate custom Hooks” is a big topic, but here are just a few Pointers.

Point zero: There is data drive

One of the things that is often mentioned when comparing Hooks to the class component development pattern is that Hooks allow us to reuse functionality more widely across components. As a result, when I first started learning Hooks, I used to overcorrect any logic that might have a reusability value. For example, I abstracted a useTodayFirstOpen function that “reminds you to open it again when the user closes the notification and opens it for the first time that day.”

/ / 🔴 Bad case
function useTodayFirstOpen() {
  const [status, setStatus] = useState();
  const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);

  useEffect(() = > {
    // Get user status
    const fetchStatus = async() = > {const res = await getUserStatus();
      setStatus(res);
    };
    fetchStatus();
    // Check whether it is opened for the first time today
    const value = window.localStorage.getItem('isTodayFirstOpen');
    if(! value) { setIsTodayFirstOpen(true);
    } else {
      constcurr = getNowDate(); setIsTodayFirstOpen(curr ! == value); }} []); useEffect(() = > {
    if (status <= 0) {
      // Do not open the second reminder
      setTimeout(() = > {
        tryToPopConfirm({
          onConfirm: () = > {
          	setStatus(1);
            updateUserStatus(1); }}); },300);
      
      window.localStorage.setItem('isTodayFirstOpen'.Date.now())
    }
  }, [status, isTodayFirstOpen]);
}
Copy the code

In fact, it doesn’t return anything, and it’s just useTodayFirstOpen() when called inside the component. In return, this feature does not have any external data flows in or out, so it can be abstracted as a high-order function instead of a custom Hooks. Therefore, it is necessary to abstract functional modules that are reusable and have a data-driven relationship with the outside world into custom Hooks.

The first point: single responsibility principle

The Single Responsibility principle (SRP) suggests that a method has only one cause for change. Custom Hooks are essentially abstract methods that facilitate reuse of logical functions within a component. However, if one Hooks takes on too many responsibilities, it is possible that a change in one responsibility can affect the implementation of another, causing unintended consequences and increasing the probability of errors during subsequent iterations of the function. As for when to split, it is more appropriate to paraphrase in Hooks “if the causes of two data changes are not the same, separate them”, referring to the recommended separation of responsibilities in SRP.

Take useTodayFirstOpen as an example. Suppose there are other Switch controls that need to be displayed and interoperated according to status:

function useTodayFirstOpen() {
  const [status, setStatus] = useState();
  const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);

  // ...
  
  const updateStatus = async (val) => {
    const res = await updateUserStatus(val);
    // dosomething...
  }
  
  return [status, updateStatus];
}
Copy the code

Suppose that the return format of getUserStatus has changed, and the Hook needs to be modified.

function useTodayFirstOpen() {
  const [status, setStatus] = useState();
  const [isTodayFirstOpen, setIsTodayFirstOpen] = useState(false);

  useEffect(() = > {
    // Get user status
    const fetchStatus = async() = > {const res = await getUserStatus();
      setStatus(res.notice);
    };
    fetchStatus();
    // ...} []);// ...
}
Copy the code

Suppose another day, the frequency of twice daily reminder of supervision feedback is too high, so the requirement is changed to “twice weekly reminder”, and the Hook needs to be reconstructed again.

function useThisWeekFirstOpen() {
  const [status, setStatus] = useState();
  const [isThisWeekFirstOpen, setIsThisWeekFirstOpen] = useState(false);

  useEffect(() = > {
    // Get user status
   	// ...
    // Check whether it is opened for the first time today
    const value = window.locaStorage.getItem('isThisWeekFirstOpen');
    if(! value) { setIsTodayFirstOpen(true);
    } else {
      const curr = getNowDate();
      setIsThisWeekFirstOpen(diffDay(curr, value) >= 7); }} []);// ...
}
Copy the code

This is a clear violation of the single responsibility principle. Consider separating status and… FirstOpen logic, to make it more general, and then abstracted as a business Hook in the form of a composite.

// User status management
function useUserStatus() {
  const [status, setStatus] = useState();

  const fetchStatus = async() = > {const res = await getUserStatus();
    setStatus(res);
  };

  useEffect(() = >{ fetchStatus(); } []);const updateStatus = useCallback(async (type, val) => {
    const res = await updateUserStatus(type, val);
    if (res) {
      console.log('Setting successful');
      fetchStatus();
    } else {
      console.log('Setup failed'); }} []);return [status, updateStatus];
}

// Second reminder
function useSecondConfirm(key, gapDay, confirmOptions = {}) {
  const [isConfirm, setIsConfirm] = useState(false);
  
  const showConfirm = useCallback(() = > {
    const curr = Date.now();
    const lastDay = window.localStorage.getItem(`${key}_lastCheckDay`);
    if(! lastDay || diffDay(curr, lastDay) > gapDay) {setTimeout(async () => {
        tryToPopConfirm({
          title: confirmOptions. The title,content: confirmOptions. The content,onConfirm: () = > setIsConfirm(true)}); },300);
      window.localStorage.setItem(`${key}_lastCheckDay`, curr);
    }
  }, [gapDay]);

  return [isConfirm, showConfirm];
}

function useStatusWithSecondConfirm(type, gapDay, confirmOptions) {
  const [userStatus, setUserStatus] = useUserStatus();  
  const [isConfirm, showConfirm] = useSecondConfirm(type, gapDay, confirmOptions);
  // The closed state reminds the user twice whether to open
  useEffect(() = > {
    console.log(userStatus);
    if (userStatus && userStatus[type] <= 0) {
      showConfirm();
    }
  }, [userStatus]);

  // Modify the user status after confirmation
  useEffect(() = > {
    if (isConfirm) {
      setUserStatus(type, 1);
    }
  }, [isConfirm]);

  return [userStatus ? userStatus[type] : null, setUserStatus];
}

/ / when used
function Component() {
  const [status, setStatus] = useStatusWithSecondConfirm(
    'notice'.1, {title: 'Whether to turn on reminders'.content: 'Open notifications to avoid missing important messages'});return (
    <>
      <label>Open message alerts</label>
      <Switch
        checked={status}
        onChange={setStatus}
      />
    </>
  );
}
Copy the code

If the interface for obtaining/setting user status changes after the modification, change useUserStatus. If the effect of the second reminder needs to be changed (for example, to report logs), modify useSecondConfirm. Adjust the second reminder if the business logic (members not secondary to remind), then only need to modify useStatusWithSecondConfirm, their definition Hooks.

N + 1. , leave a pit, later have new ideas to continue to share

conclusion

To tell you the truth, this article is a hot spot of “React Hooks”, but it has to be said that the data structures and design patterns are YYDS, which can guide us to a clear path in the development of complex systems. Since it is said that the Hooks are difficult to maintain, let’s try to let the “gods” save the mess. I’m sure you have your own answers to the question “Do design patterns help us write more elegant Hooks?” After reading the previous section, I’m sure this is not a debate about whether Hooks are stronger than class development. If you’re interested, please join ES2049. Maybe we can make it to the next debate (~ ~ â–½ ~)

Author: ES2049 / Forest Wood

The article can be reproduced at will, but please keep the original link. We welcome you to join ES2049 Studio. Please send your resume to [email protected]