The original address

mobx task manage: Loadx

Some time ago, I developed an efficiency tool, which can basically deal with relatively complex business scenarios. At present, relevant business codes have been online for several months and run well

introduce

The background,

  1. The business is complicated and involves more up and down. Usually, interface functions are called everywhere. Later, loading and await are required, which is troublesome to think about

  2. And if MOBx cannot perceive an await action, it must manually package the runInAction and make me spit blood

  3. Again, an API usually has to be walked through with a try catch finally, which is cumbersome to write and can cause multiple rerenders if used incorrectly

  4. Traditional writing has too much to manage, too little business focus, and too much fault tolerant code

Can’t do it, programmers are lazy… So there’s this Loadx productivity tool

Second, function points

  1. Facilitate the organization and management of apis

  2. Easy writing of error tolerance, finally, and subsequent action operations to focus on the business

  3. Intelligent loading for concurrent and serial requests, unified rerender

  4. You can initialize whether to load, pre-loading, and dynamic configuration

  5. Support for Class Component and hooks

  6. Convenient type prompt and various writing tolerance

The principle of

A,mobx.createAtom

  1. The core of this is the createAtom API provided by Mobx

  2. From my previous Mobx source code interpretation series, you should remember this API or the Atom class, which is the subscription-publish Atom for MOBx, on which all observableValue and so on are based

  3. Two of the core methods are reportObserved and reportChanged

  • ReportObserved: Reports to subscribers when visiting self

  • ReportChanged: Report to subscribers when changing “myself”

  1. If we implemented a subscription-publish “publish mechanism” ourselves (mobx hijacked Render to trigger forceUpdate, of course), this would be the key to controlling loading or not

Second, the request of the stack

  1. Maintain a queue of requests, which are pushed when there are interfaces, and unloaded when there are requests. Check the number of requests to see if it is currently loading

  2. With Atom, heh heh

  • Request Length 0 -> 1: Start loading and call atom.reportChanged

  • Reqeust Length 1 -> 0: End loading, also call atom.reportchanged

  1. Rerender is not called when length 1 -> n or n -> 1 is loaded

  2. When getting the loading state: call atom.reportobserved

Iii. Handling of follow-up actions

  1. If you have the try catch and runInAction, please help me to encapsulate them. Ok!

  2. But after all, you’re executing in action, so return a function

What are you waiting for, a wank

A,Loadx

  1. The shelf
exportinterface LoadxConfig { name? : string; requests? :Promise<any>[];
}

export class Loadx {
  name: string;
  private atom: IAtom;
  requests: Promise[] = [];

  constructor(config? : LoadxConfig) {
    const {
      name = 'loadx',
      requests,
    } = config || {};
    this.name = name;
    this.atom = createAtom(name);
    // Prepopulate request when new
    requests && requests.forEach((p) = > this.load(p));
  }

  get loading() {
    // When getting the status, report it up
    this.atom.reportObserved();
    return!!!!!this.requests.length;
  }

  // A single request is pushed
  load(request: Promise) {
    const { length: preLen } = this.requests;
    const thenable = Promise.resolve(request);
    this.requests.push(thenable);

    / / 0 - > 1
    if(! preLen) {this.atom.reportChanged();
    }

    return thenable
      .then(effect= > {
        let res = effect;
        runInAction(() = > {
          // Handle the action function returned from the business
          typeof effect === "function" && (res = effect.apply(this));
          / / out of the stack
          this.finish(thenable);
        });
        return res;
      })
      .catch(err= > {
        runInAction(() = > {
          this.finish(thenable);
        });
        return Promise.reject(finalErr);
      });
  }

  private finish(promise: Promise) {
    this.requests.splice(this.requests.indexOf(promise), 1);
    / / 1 - > 0
    if (!this.requests.length) {
      this.atom.reportChanged(); }}}Copy the code
  1. use
class Store {
  private loadx = new Loadx();

  count = 0;

  // To avoid direct external calls to loadX, register dependencies on atom at the React Render level
  @computed
  get loading() {
    return this.loadx.loading;
  }

  _getCount() {
    const count = await api();

    // Return action fn
    return () = > {
      this.count = count; }}getCount() {
    / / associated
    return this.loadx.load(action(this._getCount)); }}const store = new Store();
autorun(r= > {
  console.log(store.loading, store.count);
});

store.getCount();
Copy the code

Two, adornment is true meaning

Why should I call loadX. load on my own initiative? Yes, encapsulated! It doesn’t feel good to call it like @action

  1. We use mobx god decorator: createDecoratorForEnhancer, unclear friends can see my previous articles, with the simplified version of the small example
class Loadx {
  /** * Supports two decoration methods like mobx.action@action(ActionConfig) fn
   * @action fn* /
  static action = createPropDecorator(function (target, prop, descriptor, args) {
    // @action fn() {}
    // Note that this is a bind problem when you parse the call
    if (descriptor) {
      return {
        configurable: true.enumerable: false.get() {
          const fn = descriptor.value || (descriptor as any).initializer.call(this);
          / / the Object. DefineProperty
          addHiddenProp(this, prop, createLoadxFn(fn, args[0].this));
          return this[prop];
        },
        set(){}}; }else {
      // @action fn = () => {}
      Object.defineProperty(target, prop, {
        configurable: true.enumerable: false.get() {},
        set(fn) {
          addHiddenProp(this, prop, createLoadxFn(fn, args[0].this)); }}); }}); }Copy the code
  1. Then look at the core of the decorator call:createLoadxFn
exportinterface ActionConfig { loadx? : string | Loadx;// Associated store Loadxaction? : string;// Customize return action name
}

export function createLoadxFn(fn, config: ActionConfig = {}, context: any = null) {
  const id = getUid();
  return function (this: any, ... args: any[]) {
    const that = context || this;
    const { loadx: lName = "", action } = config;
    // Find loadX instance from store instance according to name
    const loadx:Loadx = that[lName];
    // Execute the store method in the action and return a Promise
    const req = runInAction(action, () = > fn.apply(that, args));

    return loadx.load(req);
  };
}
Copy the code
  1. use
class Store {
  private loadx = new Loadx();

  @Loadx.action
  getUser() {
    // Batch processing: release loading after promise. all and subsequent effect action
    await getPermission();
    const [name, age] = await Promise.all([nameApi(), ageApi()]);

    // Return action fn
    return () = > {
      if (this.perm) {
        this.name = name;
        this.age = age;
      }
    }
  }

  @Loadx.action({
    // Default error tolerance
    // name: 'loadx'
  })
  getPermission = () = > {
    const perm = await permApi();

    return () = > {
      this.perm = perm; }}}new Store().getUser();
Copy the code
  1. beautifybind

Of course, if it’s a dynamic method, you can’t use a decorator, you can only use manual bind, so let’s beautify it

class Loadx {
  bind<T extendsFnType>(fn: T, context? : any); bind<Textends FnType>(fn: T, config = {}, context: any = null
) {
  / / not config
  if(! isPlainObject(config)) { context = config; config = {}; }returncreateLoadxFn(fn, { ... config,loadx: this }, context);
}

/ / use
class Store {
  getCount = this.loadx.bind(this._getCount, this);

  _getCount() {
    const count = await api();

    return () = > {
      this.count = count; }}}const store = new Store();
store.getGender = store.loadx.bind(getGenderFromOtherPlace, store);
Copy the code

Third, improve the surrounding

  1. try catch finally
  • Since catch has been removed, it cannot be written in action FN

  • That’s what we want, too, to be concise, so that we can focus on the business itself, rather than having to cram robustness into it. So, wrap it up!

exportinterface ActionConfig { loadx? : string | Loadx;/ / associated Loadxaction? : string;// action name
  // Pass it in through configurationonError? :(err: any, ... originalArgs: any[]) = > void | Promise<void>; / / the error correctiononComplete? :(resOrErr? : any, ... originalArgs: any[]) = > void | Promise<void>; / / complete callback
}

export function createLoadxFn(fn config: ActionConfig = {}, context: any = null) {
  return function (this: any, ... args: any[]) {
    const that = context || this;
    const {
      loadx: lName = "",
      action,
      onComplete,
      onError
    } = config;
    // ...

    // Get the parameters from the decorator and hang them on the req
    onError && (req._onError = onError.bind(that));
    onComplete && (req._onComplete = onComplete.bind(that));

    return loadx.load(req);
  };
}

class Loadx {
  load<T>(request: T): LoadxLoadType<T> {
    / / get the config
    const { _onError, _onComplete } = request;
    const { length: preLen } = this.requests;

    // ...

    return thenable
      .then(effect= > {
        let res = effect;
        runInAction(() = > {
          typeof effect === "function" && (res = effect.apply(this)); _onComplete && _onComplete(res, ... _args);this.finish(thenable);
        });
        return res;
      })
      .catch(err= > {
        const finalErr = err;
        runInAction(() = >{ _onError && _onError(finalErr, ... _args); _onComplete && _onComplete(finalErr, ... _args);this.finish(thenable);
        });
        return Promise.reject(finalErr); }); }}/ / use
class Store {
  @Loadx.action({
    onError(this: Store, err) {
      console.log("onError".this, err);
    },
    onComplete(_resOrErr){}})getUser() {
    // ...}}Copy the code
  1. controlloading
  • Sometimes I want to preload a state where loading is true, such as when I’m entering the initial loading of the page

  • Or IN some cases, I don’t want to listen to loading, and I’m setting it dynamically

exportinterface ActionConfig { loadx? : string | Loadx; action? : string;// Continue to configure parametersobserve? : boolean;// Set whether to listen for loading changes. The default is truepreload? : boolean | number; onError? :(err: any, ... originalArgs: any[]) = > void | Promise<void>; onComplete? :(resOrErr? : any, ... originalArgs: any[]) = > void | Promise<void>;
}

class Loadx {
    setConfig(config: ActionConfig) {
    // Change according to the input
    if (has(config, "observe")) {
      this.observe = config.observe;
    }
    if (has(config, "preload")) {
      this.preload = config.preload;
      if (!this.preload) return;
      this.startPreload();
    }
  }
 
  private startPreload() {
    // Use setTimeout to simulate pre-sending a request
    const loadReq = new Promise((resolve) = > {
      setTimeout(resolve, this.preload);
      this.preloadFn = () = > {
        resolve();
        this.preload = false;
        this.preloadFn = null;
      };
    });
    this.load(loadReq);
  }

  private finish(promise: Promise) {
    this.requests.splice(this.requests.indexOf(promise), 1);
    if (!this.requests.length) {
      // Only when listening
      this.observe && this.atom.reportChanged(); }}}/ / use
class Store {
  @Loadx.action
  getUser() {
    this.loadx.setConfig({ observe: false });
    const count = await api();

    return () = > {
      this.count = count; }}}Copy the code
  1. Intelligent type prompt, compatible writing method
  • Now the store methods are returned as functions, which can be confusing to use in components: Promise<() => void>

  • Some people are stubborn, and do not want to return to the Action FN, but must write their own runInAction

  • Again, someone wants to return actionFn and want to return the data after the await API. Arrangement!

class Store {
  @Loadx.action
  getUser() {
    const perm = await getPermission();
    const [name, age] = await Promise.all([nameApi(), ageApi()]);

    // Return action fn
    return () = > {
      if (this.perm) {
        this.name = name;
        this.age = age;
      }
    }
  }

  @Loadx.action
  getPermission = (flag = 1) = > {
    const perm: boolean = await permApi(flag);

    // 1: pure API
    // return perm;

    // 2: Why are you so stubborn
    // runInAction(() => {
    // this.perm = perm;
    // })

    / / 3
    return () = > {
      this.perm = perm;
      returnperm; }}}class Loadx {
  load<T>(request: T): LoadxLoadType<T> {
    // ...

    return thenable
      .then(effect= > {
        let res = effect;
        runInAction(() = > {
          // The function returns the result of its execution
          typeof effect === "function" && (res = effect.apply(this)); _onComplete && _onComplete(res, ... _args);this.finish(thenable);
        });
        return res;
      })
      .catch(err= > {
        // ...}); }}Copy the code

Finally, one more wave of types. As for the specific types, it is not the focus of this article, I will separate the article in the Types Skills Summary, please look forward to it

export type UnFnReturn<T> = T extends(... args: any[]) => infer R ? R : T;export type LoadxLoadFnType<T> = T extends(... args: any[]) =>Promise<infer R> | Generator<any, infer R>
  ? (. args: Parameters
       ) = > Promise<UnFnReturn<R>>
  : T;

// Restrict store methods that return Promise
      
export type LoadxStore<T extends Record<string, any>> = {
  [K in keyof T]: LoadxLoadFnType<T[K]>;
};

/ / use
@observer
class CC extends Component<{store: LoadxStore<Store>}> {
  render() {
    const { getPermission } = this.props.store;
    // type:
    // const getPermission: (flag: string) => Promise<boolean>

    return <div></div>}}Copy the code

Fourth, the hooks

Now that we’re done, hooks are introduced, which are useLocalStore, just exposing the API

export function useLoadx(initializer: (source: P & { loadx: Loadx }) => T, loadxConfig? : ActionConfig, current? : P) :LoadxLocaleStore<T> {
  // create loadx for store
  const loadx = useMemo(() = > new Loadx(loadxConfig), []);
  current && (current.loadx = loadx);
  const store = useLocalStore(initializer, current);

  return {
    store,
    loading: loadx.loading,
    bind: (fn: T, config = {}, context: any = null) = > {
      / / not config
      if(! isPlainObject(config)) { context = config; config = {}; }returncreateLoadxFn( fn, { ... config, loadx }, context || store ); },setConfig: (config) = > loadx.setConfig(config)
  };
}

/ / use
const Fc = () = > {
  const { store, loading, bind } = useLoadx(() = > ({
    data: { name: "empty"}}));const getDataWithLoading = bind(async() = > {const res = await wait(1000, { name: 'lawler'.age: 20 });
    return () = > {
      store.data = res;
    };
  });

  const { data } = store;
  return useObserver(() = > (
    <div>
      <div>
        {`loading: ${loading}, data: ${data.name}`}
      </div>
      <button onClick={getDataWithLoading}>FC loadx</button>
    </div>
  ));
};

export default observer(Fc);
Copy the code

The last

  1. Loadx source Loadx source

  2. If the response is good, you can consider open source (yes, install 13 only), and welcome folk improvement

  3. In addition, I did not find out until I finished writing this util that there is a big guy on Github who has completed a similar tool mox-Task. After checking it out, it is completely different from his source code and use gesture, but it can also be used as a reference

  4. Like small partners, remember to leave your small ❤️ oh ~