react.js + hooksHandwritten responsive reactive

Redux is too cumbersome, Mbox is cool but we may not need to introduce new packages, so let us implement a set of hooks through the proxy in react.js ourselves to achieve something like Reactive state of vue:

Implement reactive hooks

Proxy class declaration

The proxy state class should provide accessible state and an interface for subscribing to changes.

export type Listener<T> = (state: T) => any;

export interface ReactiveCtxModel<T = any> {<!-- -->
  value: T;
  subscribe(listener: Listener<T>): () => void;
}

Proxy class implementation

Use es6 class to implement the proxy class. es6 class provides get/set accessors for attributes, so that when we access through obj.key, we do not access directly, but through the proxy. The real The value is set as a private property via private.

In the constructor of the class, we pass in the return obtained by using React.useState. Yes, if we want the page to respond to data changes in react, we still need useState. If we do not pass setState, this Reactive will be lazy because it cannot Trigger a re-rendering of the page.

In addition to the state to be saved, the private properties also have the listeners array to save the functions to be triggered for monitoring changes. These functions are called every time the state is called by the set accessor.

export class Reactive<T = any> implements ReactiveCtxModel {<!-- -->
  private _state: T;
  private _setState: any = (newState: T) => {<!-- -->
    this._state = newState;
  };
  private _listeners: Listener<Readonly<T>>[] = [];

  constructor(state: T, setState?: any) {<!-- -->
    this._state = state;
    setState ? (this._setState = setState) : void 0;
  }

  get value(): T {<!-- -->
    return this._state;
  }

  set value(newState: T) {<!-- -->
    this._setState?.(newState);
    this._listeners.forEach((listener) => listener(newState));
  }

  subscribe(listener: Listener<T>) {<!-- -->
    this._listeners.push(listener);
    return () => {<!-- -->
      this._listeners = this._listeners.filter((l) => l !== listener);
    };
  }

  static isReactive(obj: any) {<!-- -->
    return Reactive.prototype.isPrototypeOf(obj);
  }
}

Implement the hook function for creating a proxy

It is too troublesome to manually create useState() every time in the code and then new Reactive(). We encapsulate these operations into a hook Reactify and then assign to reactive, so that we can directly use reactive(initialValue) to create reactive objects. (Why create Reactify first? Because react agrees that the top space of react’s use hook should be named useXXX or start with capital letter, because I like the name reactive, so I did an exchange)

const Reactify = <T = any>(initialValue: T): Reactive<T> => {<!-- -->
  const [state, setState] = React.useState<T>(initialValue);
  const observer = new Reactive(state, setState);
  return observer;
};
/**
 * reactive is same with Reactify
 */
export const reactive = Reactify;

example:

const Demo: React.FC = () => {<!-- -->
  let state = reactive(0);
  const num = state.value;

  return (
    <>
      <Button
        onClick={<!-- -->() => {<!-- -->
          state.value = state.value + 1;
        }}
      >
        {<!-- -->num}
      </Button>
    </>
  );
};

Implement listening function

Calling subscribe directly on the Reactive object is great, but sometimes I prefer that this operation can be extracted, so I have the following listen function, passing in the Reactive object to be listened to, and then chaining in the callback to be triggered in then , more elegant in appearance.

/**
 * When store.state changes, call the given function.
 * @param target listened Reactive store
 * @returns unlistener
 */
export function listen<T = any>(target: Omit<Reactive<T>, "_state" | "_setState">) {<!-- -->
  return {<!-- -->
    then: (...fns: ((value: T) => any)[]) => {<!-- -->
      const fn = (value: T) => fns.forEach((f) => f(value));
      const dispose = target.subscribe(fn);
      return dispose;
    },
  };
}

example:

 listen(obj).then((newVal) => {<!-- -->
    console.log(`newVal: ${<!-- -->newVal}`);
  });

Pass Reactive with Context

The above reactive can only be used locally in a single component. Even if it is passed to a subcomponent through props, the subcomponent only has read-only rights. If we need to share Reactive proxies across components, we can use React.Context:

Create default Context

import {<!-- --> createContext } from "react";
import {<!-- --> Listener, Reactive } from "./model";

export const createReactiveContext = <T = any>(initialValue?: T) => {<!-- -->
  const reactiveObject = new Reactive(initialValue);
  return createContext<ReactiveCtxModel<T> | undefined>(reactiveObject as any);
};

const ReactiveCtx = createReactiveContext();

export default ReactiveCtx;

Implement useReactive hook

useReactive can receive an initial value. If the initial value is obtained, a new context and Reactive object will be opened. Otherwise, the ReactiveCtx created in the previous step will be used.

/**
 * Accept a value and return a reactive object. When initalValue is valid a new reactive object will be created.
 */
export const useReactive = <T = any>(initialValue?: T): Reactive<T> => {<!-- -->
  const [state, setState] = React.useState<T>(initialValue  (undefined as T));
  const reactiveObj = new Reactive(state, setState);
  const defaultContextModel = React.useContext((initialValue as any)  ReactiveCtx);
  if (initialValue !== undefined & amp; & amp; initialValue !== null) {<!-- -->
    return reactiveObj as Reactive<T>;
  }
  return defaultContextModel as Reactive<T>;
};

Implement useReactiveContext hook

The context created after useReactive receives the initial value cannot be obtained by other components. To allow other components to share non-default context, we need to create and export a new context externally, and implement a useReactiveContext hook to receive the new context, so that New context can be shared, and if no new context is passed in, we will continue to use the default ReactiveCtx.

export const useReativeContext = <T = any>(context?: React.Context<ReactiveCtxModel<T> | undefined>): Reactive<T> => {<!-- -->
  const reactiveCtxModel = React.useContext(context || ReactiveCtx);
  return reactiveCtxModel as Reactive<T>;
};

Now, we replace the raective used in the original demo with useReactive, and then we can freely share Reactive across components.
example:

const Demo: React.FC = () => {<!-- -->
  let state = useReactive(0);
  const num = state.value;

  listen(state).then((newVal) => {<!-- -->
    console(`newVal: ${<!-- -->newVal}`);
  });

  return (
    <>
      <Button
        $click={<!-- -->() => {<!-- -->
          state.value = state.value + 1;
        }}
      >
        {<!-- -->num}
      </Button>
      <ReactiveCtx.Provider value={<!-- -->state}>
        <Kid />
      </ReactiveCtx.Provider>
    </>
  );
};

Kid:

function Kid() {<!-- -->
  const state = useReactive<number>();
  return (
    <>
      <Tag
        light
        style={<!-- -->{<!-- --> cursor: "pointer" }}
        onClick={<!-- -->() => {<!-- -->
          state.value + + ;
        }}
      >
        state : {<!-- -->state.value}
      </Tag>
      <Tag
        light
        style={<!-- -->{<!-- --> cursor: "pointer" }}
        onClick={<!-- -->() => {<!-- -->
          state2.value + + ;
        }}
      >
        state2 : {<!-- -->state2.value}
      </Tag>
      <context.Provider value={<!-- -->state2}>
        <KidKid />
      </context.Provider>
    </>
  );
}

Bingo! At this point we have basically implemented reactive, bye~