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:
- The abstraction is not enough to get the options, and the error handling function can be completely isolated and become a generic method.
- A synchronous method becomes asynchronous after the transformation. So in theory, distinguish between synchronous and asynchronous schemes.
- What if the error handler is abnormal
- Functional limitations
Let’s figure it out.
General charm
We captured the range:
- Class statically synchronized methods
- Class static asynchronous methods
- Class
- Class
- Class synchronization attribute assignment method
- Class async attribute assignment method
- Getter method for Class
- 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
- Separation of definitions based on useCatch
- Write another one for each Hook
useXXX
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:
- Support for capturing entire classes directly
- Fix related problems with utility
- Separate code and examples, encapsulated as libraries
- Try to use
Proxy
implementation
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