This is the 9th day of my participation in the More text Challenge. For details, see more text Challenge

The companion React, the elegant catch exception

preface

In the React project, it was annoying because event handlers always had to write try/catch.

It can be thrown to window.onError or window.addeventListener (“error”), but the capture of error details and error compensation is extremely unfriendly.

So based on the ES standard decorator, out of an event handler catch scheme, see the React, elegant catch exception.

In the comments section, there was a joke about Class. Hooks 666.

I want to keep up with The Times. I want to support Hooks, getters, etc.

add

Originally designed to catch and handle exceptions to the event program, it can be used on virtually any Class method.

Issue review

React, the elegant exception catch scheme has problems:

  1. The abstraction is not enough to get the options, and the error handling function can be completely isolated and become a generic method.
  2. A synchronous method becomes asynchronous after the transformation. So in theory, distinguish between synchronous and asynchronous schemes.
  3. What if the error handler is abnormal
  4. Functional limitations

Let’s figure it out.

General charm

We captured the range:

  1. Class statically synchronized methods
  2. Class static asynchronous methods
  3. Class
  4. Class
  5. Class synchronization attribute assignment method
  6. Class async attribute assignment method
  7. Getter method for Class
  8. Hooks method

The getter here is very similar to the vue evaluated value, so don’t say I don’t evaluate properties in React, haha.

Here we go. Here we go.

Let’s look at the Class component first


interface State {
    price: number;
    count: number;
}

export default class ClassT extends BaseComponent<{}, State> {
    constructor(props) {
        super(props);
        this.state = {
            price: 100.count: 1
        }
        this.onIncrease = this.onIncrease.bind(this);
        this.onDecrease = this.onDecrease.bind(this);
    }

    componentDidMount() {
        ClassT.printSomething();
        ClassT.asyncPrintSomething();

        this.doSomethings();
        this.asyncDoSomethings();
    }

    @catchMethod({ message: "printSomething error".toast: true })
    static printSomething() {
        throw new CatchError("PrintSomething error: Active throw");
        console.log("printSomething:".Date.now());
    }

    @catchMethod({ message: "asyncPrintSomething error".toast: true })
    static async asyncPrintSomething() {
        const { run } = delay(1000);
        await run();
        throw new CatchError("AsyncPrintSomething error: Active throw");
        console.log("asyncPrintSomething:".Date.now());
    }

    @catchGetter({ message: "Failure to calculate price".toast: true })
    get totalPrice() {
        const { price, count } = this.state;
        // throw new Error("A");
        return price * count;
    }

    @catchMethod("Failed to increase quantity")
    async onIncrease() {

        const { run } = delay(1000);
        await run();

        this.setState({
            count: this.state.count + 1})}@catchMethod("Failure to reduce quantity")
    onDecrease() {
        this.setState({
            count: this.state.count - 1})}@catchInitializer({ message: "catchInitializer error".toast: true })
    doSomethings = () = > {
        console.log("do some things");
        throw new CatchError("CatchInitializer error: Actively thrown");
    }

    @catchInitializer({ message: "catchInitializer async error".toast: true })
    asyncDoSomethings = async() = > {const { run } = delay(1000);
        await run();
        throw new CatchError("CatchInitializer async error: Actively raised");
    }

    render() {
        const { onIncrease, onDecrease } = this;
        const totalPrice = this.totalPrice;

        return <div style={{
            padding: "150px",
            lineHeight: "30px",
            fontSize: "20px}} ">
            <div>Price: {this. State. Price}</div>
            <div>Quantity: 1.</div>
            <div>
                <button onClick={onIncrease}>Increase the quantity</button>
                <button onClick={onDecrease}>To reduce the number of</button>
            </div>
            <div>{totalPrice}</div>
        </div>}}Copy the code

Then there are the functional components, which are the Hooks that people are interested in, that wrap around useCatch, and are based on useMemo underneath

const HooksTestView: React.FC<Props> = function (props) {

    const [count, setCount] = useState(0);

    
    const doSomething  = useCatch(async function(){
        console.log("doSomething: begin");
        throw new CatchError("doSomething error")
        console.log("doSomething: end");
    }, [], {
        toast: true
    })

    const onClick = useCatch(async (ev) => {
        console.log(ev.target);
        setCount(count + 1);

        doSomething();


        const d = delay(3000.() = > {
            setCount(count= > count + 1);
            console.log()
        });
        console.log("delay begin:".Date.now())
        await d.run();
        console.log("delay end:".Date.now())
        console.log("TestView".this);
        (d as any).xxx.xxx.x.x.x.x.x.x.x.x.x.x.x
        // Throw new CatchError(" custom exception, you know? ")
    },
        [count],
        {
            message: "I am so sorry".toast: true
        });

    return <div>
        <div><button onClick={onClick}>Am I</button></div>
        <div>{count}</div>
    </div>
}

export default React.memo(HooksTestView);
Copy the code

Let’s take a look at what we’ve done, and why we’re talking about optimization. Because after optimizing the previous code, the code readability, reusability, scalability is greatly enhanced.

To optimize the

Encapsulates the getOptions method

// Options type whitelist
const W_TYPES = ["string"."object"];

export function getOptions(options: string | CatchOptions) {
    const type = typeof options;
    let opt: CatchOptions;
    
    if (options == null| |! W_TYPES.includes(type)) { // Null or not a string or object
        opt = DEFAULT_ERRPR_CATCH_OPTIONS;
    } else if (typeof options === "string") {  / / stringopt = { ... DEFAULT_ERRPR_CATCH_OPTIONS,message: options || DEFAULT_ERRPR_CATCH_OPTIONS.message,
        }
    } else { // A valid objectopt = { ... DEFAULT_ERRPR_CATCH_OPTIONS, ... options } }return opt;
}
Copy the code

Define the default handler

/ * * * *@param Err default error handler *@param options 
 */
function defaultErrorHanlder(err: any, options: CatchOptions) {
    const message = err.message || options.message;
    console.error("defaultErrorHanlder:", message, err);
}

Copy the code

Distinguish between synchronous and asynchronous methods

export function observerHandler(fn: AnyFunction, context: any, callback: ErrorHandler) {
    return async function (. args:any[]) {
        try {
            const r = await fn.call(context || this. args);return r;
        } catch(err) { callback(err); }}; }export function observerSyncHandler(fn: AnyFunction, context: any, callback: ErrorHandler) {
    return function (. args:any[]) {
        try {
            const r = fn.call(context || this. args);return r;
        } catch(err) { callback(err); }}; }Copy the code

Multi-level option definition capability

export default function createErrorCatch(handler: ErrorHandlerWithOptions, baseOptions: CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS) {

    return {
        catchMethod(options: CatchOptions | string = DEFAULT_ERRPR_CATCH_OPTIONS) {
            returncatchMethod({ ... baseOptions, ... getOptions(options) }, handler) } } }Copy the code

Custom error handlers

export function commonErrorHandler(error: any, options: CatchOptions) {    
    try{
        let message: string;
        if (error.__type__ == "__CATCH_ERROR__") {
            error = error as CatchError;
            constmOpt = { ... options, ... (error.options || {}) }; message = error.message || mOpt.message ;if (mOpt.log) {
                console.error("asyncMethodCatch:", message , error);
            }

            if (mOpt.report) {
                // TODO::
            }

            if(mOpt.toast) { Toast.error(message); }}else {

            message = options.message ||  error.message;
            console.error("asyncMethodCatch:", message, error);

            if(options.toast) { Toast.error(message); }}}catch(err){
        console.error("commonErrorHandler error:", err); }}const errorCatchInstance = createErrorCatch(commonErrorHandler);

export const catchMethod = errorCatchInstance.catchMethod; 
Copy the code

To enhance

Support the getter

Let’s take a look at the use of catchGetter

class Test {

    constructor(props) {
        super(props);
        this.state = {
            price: 100.count: 1
        }

        this.onClick = this.onClick.bind(this);
    }

    @catchGetter({ message: "Failure to calculate price".toast: true })
    get totalPrice() {
        const { price, count } = this.state;
        throw new Error("A");
        return price * count;
    }
    
      render() {   
        const totalPrice = this.totalPrice;

        return <div>
            <div>Price: {this. State. Price}</div>
            <div>Quantity: 1.</div>
            <div>{totalPrice}</div>
        </div>}}Copy the code

implementation

/**
 * class {  get method(){} }
 * @param options 
 * @param hanlder 
 * @returns * /
export function catchGetter(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, 
hanlder: ErrorHandlerWithOptions = defaultErrorHanlder) {

    let opt: CatchOptions = getOptions(options);

    return function (_target: any, _name: string, descriptor: PropertyDescriptor) {
        const { constructor } = _target;
        const { get: oldFn } = descriptor;

        defineProperty(descriptor, "get", {
            value: function () {
                // Class.prototype.key lookup
                // Someone accesses the property directly on the prototype on which it is
                // actually defined on, i.e. Class.prototype.hasOwnProperty(key)
                if (this === _target) {
                    return oldFn();
                }
                // Class.prototype.key lookup
                // Someone accesses the property directly on a prototype but it was found
                // up the chain, not defined directly on it
                // i.e. Class.prototype.hasOwnProperty(key) == false && key in Class.prototype
                if (
                    this.constructor ! = =constructor &&
                    getPrototypeOf(this).constructor= = =constructor
                ) {
                    return oldFn();
                }
                const boundFn = observerSyncHandler(oldFn, this.function (error: Error) {
                    hanlder(error, opt)
                });
                (boundFn as any)._bound = true;
            
                returnboundFn(); }});returndescriptor; }}Copy the code

Support for attribute definition and assignment

For details, see babel-plugin-proposal-class-properties

For demo, see class-error-catch

class Test{
    @catchInitializer("nono")
    doSomethings = () = > {
        console.log("do some things"); }}Copy the code

implementation

export function catchInitializer(options: string | CatchOptions = DEFAULT_ERRPR_CATCH_OPTIONS, hanlder: ErrorHandlerWithOptions = defaultErrorHanlder){

    const opt: CatchOptions = getOptions(options);

     return function (_target: any, _name: string, descriptor: any) {

        console.log("debug....");
        const initValue = descriptor.initializer();
        if (typeofinitValue ! = ="function") {
            return descriptor;
        }

        descriptor.initializer = function() {
            initValue.bound = true;
            return observerSyncHandler(initValue, this.function (error: Error) {
                hanlder(error, opt)
            });
        };
        returndescriptor; }}Copy the code

Support the Hooks

use


const TestView: React.FC<Props> = function (props) {

    const [count, setCount] = useState(0);

    
    const doSomething  = useCatch(async function(){
        console.log("doSomething: begin");
        throw new CatchError("doSomething error")
        console.log("doSomething: end");
    }, [], {
        toast: true
    })

    const onClick = useCatch(async (ev) => {
        console.log(ev.target);
        setCount(count + 1);

        doSomething();

        const d = delay(3000.() = > {
            setCount(count= > count + 1);
            console.log()
        });
        console.log("delay begin:".Date.now())

        await d.run();
        
        console.log("delay end:".Date.now())
        console.log("TestView".this)
        throw new CatchError("Custom exceptions, you know?")
    },
        [count],
        {
            message: "I am so sorry".toast: true
        });

    return <div>
        <div><button onClick={onClick}>Am I</button></div>
        <div>{count}</div>
    </div>
}

export default React.memo(TestView);

Copy the code

Implementation: The basic idea is to use useMemo and previously encapsulated observerHandler in a few lines of code.

export function useCatch<T extends (. args:any[]) = >any> (callback: T, deps: DependencyList, options: CatchOptions =DEFAULT_ERRPR_CATCH_OPTIONS) :T {    

    const opt =  useMemo( () = > getOptions(options), [options]);
    
    const fn = useMemo((. _args:any[]) = > {
        const proxy = observerHandler(callback, undefined.function (error: Error) {
            commonErrorHandler(error, opt)
        });
        return proxy;

    }, [callback, deps, opt]) as T;

    return fn;
}
Copy the code

Now what you might say is, you just implemented the exception catching of the method, my useEffect, useCallbak, useLayout, whatever, you don’t care?

Actually, at this point, there are two basic ideas

  1. Separation of definitions based on useCatch
  2. Write another one for each HookuseXXX

At this point, I think, you have not been difficult.

I’m just giving you an idea, an idea that doesn’t seem complicated, that works.

About the source

Because the code is currently running directly into our actual project, we haven’t had time to separate the code into a separate project. Want to all source students can contact me.

After all source code, the example is independent.

subsequent

I’m sure some people will say, well, you’re using object.defineProperty, out, and you can see that vue is implemented with a Proxy.

Yes, Proxy is powerful, but there are two things I can think of here that make Proxy worse than object.defineProperty and decorators.

1. Compatibility 2

Follow-up:

  1. Support for capturing entire classes directly
  2. Fix related problems with utility
  3. Separate code and examples, encapsulated as libraries
  4. Try to useProxyimplementation

Libraries with similar functionality

  • catch-decorator

Only the methods are captured, and the processing is relatively rudimentary

  • catch-decorator-ts

Same as above

  • catch-error-decorator

Use AsyncFunction to determine the default value returned after failure.

  • auto-inject-async-catch-loader

Main capture asynchronous method, principle is webPack Loader, traversal AST. Other async-catch-loader, babel-plugin-Promise-catcher and so on work in a similar way.

Write in the last

Writing is not easy, if I feel good, praise a review, is my biggest motivation.

babel-plugin-proposal-class-properties)

setpublicclassfields