Detailed explanation of small state management based on React Hooks

Detailed explanation of small state management based on React Hooks

This article mainly introduces a state sharing solution based on React Hooks, introduces its implementation, and summarizes the usage experience, with the aim of providing an additional option for state management.

Implementing state sharing based on React Hooks

Sharing state between React components is a common problem, and there are many solutions, such as Redux, MobX, etc. These solutions are very professional and have stood the test of time, but I personally think they are not suitable for some less complex projects and may introduce some additional complexity.

In fact, many times, I don’t want to define mutations and actions, I don’t want to use a layer of context, and I don’t want to write connect and mapStateToProps; what I want is a lightweight and simple state sharing solution that is easy to reference and use.

With the birth and popularity of Hooks, my idea came true.

Next, I will introduce the solution I am currently using. By combining Hooks with the publish/subscribe model, a simple and practical state sharing solution can be implemented. Because there is not much code, the complete implementation is given below.

import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useReducer,
  useRef,
  useState,
} from 'react';

/**
 * @see https://github.com/facebook/react/blob/bb88ce95a87934a655ef842af776c164391131ac/packages/shared/objectIs.js
 * inlined Object.is polyfill to avoid requiring consumers to ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any): boolean {
  return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}

const objectIs = typeof Object.is === 'function' ? Object.is : is;

/**
 * @see https://github.com/facebook/react/blob/933880b4544a83ce54c8a47f348effe725a58843/packages/shared/shallowEqual.js
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values ​​which are not strictly equal between the arguments.
 * Returns true when the values ​​of all keys are strictly equal.
 */
function shallowEqual(objA: any, objB: any): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

const useForceUpdate = () => useReducer(() => ({}), {})[1] as VoidFunction;

type ISubscriber<T> = (prevState: T, nextState: T) => void;

export interface ISharedState<T> {
  /** Get data statically, suitable for use in non-components or when data is not bound to a view*/
  get: () => T;
  /** Modify data and assign new value*/
  set: Dispatch<SetStateAction<T>>;
  /** (Shallow) merge update data*/
  update: Dispatch<Partial<T>>;
  /** The hooks method obtains data, which is suitable for use in components. The component will be automatically re-rendered when the data changes*/
  use: () => T;
  /** Subscription data changes */
  subscribe: (cb: ISubscriber<T>) => () => void;
  /** Unsubscribe data changes */
  unsubscribe: (cb: ISubscriber<T>) => void;
  /** Filter out some states */
  usePick<R>(picker: (state: T) => R, deps?: readonly any[]): R;
}

export type IReadonlyState<T> = Omit<ISharedState<T>, 'set' | 'update'>;

/**
 * Create a state that can be shared between different instances * @param initialState initial data */
export const createSharedState = <T>(initialState: T): ISharedState<T> => {
  let state = initialState;
  const subscribers: ISubscriber<T>[] = [];

  //Subscribe to state changes const subscribe = (subscriber: ISubscriber<T>) => {
    subscribers.push(subscriber);
    return () => unsubscribe(subscriber);
  };

  // Unsubscribe from state changes const unsubscribe = (subscriber: ISubscriber<T>) => {
    const index = subscribers.indexOf(subscriber);
    index > -1 && subscribers.splice(index, 1);
  };

  // Get the latest state
  const get = () => state;

  // Change state
  const set = (next: SetStateAction<T>) => {
    const prevState = state;
    // @ts-ignore
    const nextState = typeof next === 'function' ? next(prevState) : next;
    if (objectIs(state, nextState)) {
      return;
    }
    state = nextState;
    subscribers.forEach((cb) => cb(prevState, state));
  };

  //Get the latest state of hooks usage const use = () => {
    const forceUpdate = useForceUpdate();

    useEffect(() => {
      let isMounted = true;
      // Update the component immediately after mounting to avoid not being able to use the first updated data forceUpdate();
      const un = subscribe(() => {
        if (!isMounted) return;
        forceUpdate();
      });
      return () => {
        un();
        isMounted = false;
      };
    }, []);

    return state;
  };

  const usePick = <R>(picker: (s: T) => R, deps = []) => {
    const ref = useRef<any>({});

    ref.current.picker = picker;

    const [pickedState, setPickedState] = useState<R>(() =>
      ref.current.picker(state),
    );

    ref.current.oldState = pickedState;

    const sub = useCallback(() => {
      const pickedOld = ref.current.oldState;
      const pickedNew = ref.current.picker(state);
      if (!shallowEqual(pickedOld, pickedNew)) {
        // Avoid pickedNew being a function
        setPickedState(() => pickedNew);
      }
    }, []);

    useEffect(() => {
      const un = subscribe(sub);
      return un;
    }, []);

    useEffect(() => {
      sub();
    }, [...deps]);

    return pickedState;
  };

  return {
    get,
    set,
    update: (input: Partial<T>) => {
      set((pre) => ({
        ...pre,
        ...input,
      }));
    },
    use,
    subscribe,
    unsubscribe,
    usePick,
  };
};

With createSharedState, the next step is to easily create a shared state, and the way to use it in the component is also very direct.

// Create a state instance const countState = createSharedState(0);

const A = () => {
  //Use hooks in components to get responsive data const count = countState.use();
  return <div>A: {count}</div>;
};

const B = () => {
  // Use the set method to modify data return <button onClick={() => countState.set(count + 1)}>Add</button>;
};

const C = () => {
  return (
    <button
      onClick={() => {
        // Use the get method to get data console.log(countState.get());
      }}
    >
      Get
    </button>
  );
};

const App = () => {
  return (
    <>
      <A />
      <B />
      <C />
    </>
  );
};

For complex objects, a method is also provided to monitor data changes in a specified part in the component to avoid redundant rendering caused by changes in other fields:

const complexState = createSharedState({
  a: 0,
  b: {
    c: 0,
  },
});

const A = () => {
  const a = complexState.usePick((state) => state.a);
  return <div>A: {a}</div>;
};

However, for complex objects, it is generally recommended to use a composite derivation approach, deriving a complex object from multiple simple states. In addition, sometimes we need a calculation result based on the original data, so a way to derive data is also provided here.

By explicitly declaring dependencies, you can listen to the data source and pass it into the calculation function to get a responsive derived result.

/**
 * State derived (or computed)
 * ```ts
 * const count1 = createSharedState(1);
 * const count2 = createSharedState(2);
 * const count3 = createDerivedState([count1, count2], ([n1, n2]) => n1 + n2);
 * ```
 * @param stores
 * @param fn
 * @param initialValue
 * @returns
 */
export function createDerivedState<T = any>(
  stores: IReadonlyState<any>[],
  fn: (values: any[]) => T,
  opts?: {
    /**
     * Whether to respond synchronously * @default false
     */
    sync?: boolean;
  },
): IReadonlyState<T> & {
  stop: () => void;
} {
  const { sync } = { sync: false, ...opts };
  let values: any[] = stores.map((it) => it.get());
  const innerModel = createSharedState<T>(fn(values));

  let promise: Promise<void> | null = null;

  const uns = stores.map((it, i) => {
    return it.subscribe((_old, newValue) => {
      values[i] = newValue;

      if (sync) {
        innerModel.set(() => fn(values));
        return;
      }

      // Asynchronous update promise =
        promise ||
        Promise.resolve().then(() => {
          innerModel.set(() => fn(values));
          promise = null;
        });
    });
  });

  return {
    get: innerModel.get,
    use: innerModel.use,
    subscribe: innerModel.subscribe,
    unsubscribe: innerModel.unsubscribe,
    usePick: innerModel.usePick,
    stop: () => {
      uns.forEach((un) => un());
    },
  };
}

At this point, the introduction to the implementation of the state sharing method based on Hooks is over.

In recent projects, there are scenarios that require state sharing, and I have chosen the above method. The same set of implementations can be used in both Web projects and small program Taro projects, and it has been relatively smooth.

User experience

Finally, let’s summarize several characteristics of this approach:

1. Simple implementation, without introducing other concepts, only combined with the publish/subscribe model based on Hooks, and can be used in React-like scenarios, such as Taro;

2. Easy to use, because there is no other concept, you can get the reference of state by calling the create method directly, and call the use method on the state instance to complete the binding of components and data;

3. Type-friendly. No extra types need to be defined when creating a state, and the type can be automatically deduced when using it.

4. Avoid the "closure trap" of Hooks, because the reference of state is constant, and the latest value can always be obtained through the state's get method:

const countState = createSharedState(0);

const App = () => {
  useEffect(() => {
    setInterval(() => {
      console.log(countState.get());
    }, 1000);
  }, []);
  // return ...
};

5. Directly support sharing between multiple React applications. When using some pop-up boxes, it is easier to have multiple React applications:

const countState = createSharedState(0);

const Content = () => {
  const count = countState.use();
  return <div>{count}</div>;
};

const A = () => (
  <button
    onClick={() => {
      Dialog.info({
        title: 'Alert',
        content: <Content />,
      });
    }}
  >
    open
  </button>
);

6. Support obtaining/updating data in scenes outside components

7. There are great limitations in the SSR scenario: the state is created in a fragmented and decentralized manner, and the state lifecycle does not follow the React application, which makes it impossible to write SSR application code in an isomorphic way

The above is the entire content of this article. In fact, Hooks has been popular for so long that there are already many new state sharing implementations in the community. This is only used as a reference.

According to the above characteristics, this method has obvious advantages and also fatal defects (for SSR), but in actual use, the appropriate method can be selected according to the specific situation. For example, in Taro2's applet application, there is no need to worry about SSR, so I prefer this approach; if in an SSR isomorphic project, then I must choose Redux.

In short, there is one more option, and how to use it depends on the specific situation.

The above is the detailed content of the detailed explanation of small state management based on React Hooks. For more information about React Hooks small state management, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • React uses Hooks to simplify state binding of controlled components
  • Teach you to create custom hooks in react
  • React Hooks Detailed Explanation
  • Common pitfalls of using React Hooks
  • React hooks pros and cons
  • React Hooks Common Use Scenarios (Summary)
  • React hooks introductory tutorial
  • Detailed explanation of the usage of react hooks

<<:  MySQL database aggregate query and union query operations

>>:  Introduction to JWT Verification Using Nginx and Lua

Recommend

JS implements city list effect based on VUE component

This article example shares the specific code for...

Web page production TD can also overflow hidden display

Perhaps when I name this article like this, someon...

How to uninstall MySQL 5.7.19 under Linux

1. Find out whether MySQL was installed before Co...

JavaScript to implement voice queuing system

Table of contents introduce Key Features Effect d...

How to deal with too many Docker logs causing the disk to fill up

I have a server with multiple docker containers d...

Linux platform mysql enable remote login

During the development process, I often encounter...

Problems with nodejs + koa + typescript integration and automatic restart

Table of contents Version Notes Create a project ...

Detailed explanation of the basic implementation principle of MySQL DISTINCT

Preface DISTINCT is actually very similar to the ...

Parsing MySQL binlog

Table of contents 1. Introduction to binlog 2. Bi...

Kali Linux Vmware virtual machine installation (illustration and text)

Preparation: 1. Install VMware workstation softwa...

Implementation idea of ​​left alignment of the last row of flex box layout

Using flex layout, if it is a nine-square grid, i...

A brief discussion on creating cluster in nodejs

Table of contents cluster Cluster Details Events ...