Search code examples
javascriptproxyclosurespass-by-referencereduce

Can i optimize my Javascript class by only running reducer once, but keep the desired functionality?


Note: After working with react, redux, react-redux, redux-saga, redux-thunk, with and without typescript, I am trying to create a new and better framework for frontend async logic, with the benefits of redux-saga (without redux-saga) and excellent typescript support. I have made a promising MVP, but i have yet to figure out the takeLatest functionality from redux-saga. This is the background for the following question.

My question, is a subject of performance.

I have created a class called Actions and an example of how it can be used.

class Actions {
    constructor() {
        const actionNames = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango', 'uniform', 'victor', 'whiskey', 'x-ray', 'yankee', 'zulu'];
        this.handler = (action) => console.log(`${action} was just executed.`)
        Object.assign(this, {
            cancel: () => {
                this.handler = (cur) => console.log('Sorry, all actions are «cancelled».')
            },
            actions: actionNames.reduce((acc, cur) => ({
                ...acc,
                [cur]: () => this.handler(cur)
            }), {})
        })
        console.log('The reduce function was just executed!')
    }
}

const {actions: actionsA, cancel: cancelA} = new Actions() // output: The reduce function was just executed!

actionsA.alpha()    // output: alpha was just executed.
actionsA.foxtrot()  // output: foxtrot was just executed.

cancelA()

actionsA.alpha()    // output: Sorry, all actions are «cancelled».
actionsA.foxtrot()  // output: Sorry, all actions are «cancelled».

const {actions: actionsB, cancel: cancelB} = new Actions() // output: The reduce function was just executed!

actionsB.alpha()    // output: alpha was just executed.
actionsB.foxtrot()  // output: foxtrot was just executed.

cancelB()

actionsB.alpha()    // output: Sorry, all actions are «cancelled».
actionsB.foxtrot()  // output: Sorry, all actions are «cancelled».

You may run the added code snippet to verify the output.

Here comes the question!

Is it possible to - somehow - avoid running the reducer each time a new instance of Actions is created? I understand that this would involve changing the implementation of Actions, but i would very much like the instance to behave as specified in the code snippet and example above.

My thinking is that this might be possible, and would be a great optimiziation given that new Actions() would be called alot of times given the nature of a frontend application.

What i would try is to create a wrapping function/closure/factory/generator or a wrapping class that generates the Actions class. See the example below for an example of what I am currently trying.

class ActionsGenerator {
    constructor() {
        const actionNames = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango', 'uniform', 'victor', 'whiskey', 'x-ray', 'yankee', 'zulu'];
        this.handler = (action) => {}
        const actions = actionNames.reduce((acc, cur) => ({
            ...acc,
            [cur]: () => this.handler(cur)
        }), {})
        console.log('The reduce function was just executed in generator!')
        Object.assign(this, {
            Actions: class Actions {
                constructor() {
                    // somehow implement the already created actions here. Is that possible?
                }
            }
        })
    }
}

const { Actions } = new ActionsGenerator()  // output: The reduce function was just executed in generator!

const {actions: actionsA, cancel: cancelA} = new Actions() // <-- No output this time!

...

I would expect the output of my desired program change also.

  1. The reduce function was just executed in generator!
  2. alpha was just executed.
  3. foxtrot was just executed.
  4. Sorry, all actions are «cancelled».
  5. Sorry, all actions are «cancelled».
  6. alpha was just executed.
  7. foxtrot was just executed.
  8. Sorry, all actions are «cancelled».
  9. Sorry, all actions are «cancelled».

If someone happens to know for certain that this is impossible, i would really appreciate an explanation.

Is this possible?

Thanks:)


Solution

  • You could use a proxy as value for the actions property. This way you don't have to construct the (potentially large) object. Instead the proxy layer will return the right function when a particular action is read.

    Not essential, but I would not overwrite the handler, but use a cancelled property instead, which the handler will use to decide what to do.

    class Actions {
        static #actionNames = new Set(['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima', 'mike', 'november', 'oscar', 'papa', 'quebec', 'romeo', 'sierra', 'tango', 'uniform', 'victor', 'whiskey', 'x-ray', 'yankee', 'zulu']);
        
        #cancelled = false;
        actions = new Proxy(this, {
            get(obj, action) {
                if (Actions.#actionNames.has(action)) return () => obj.handler(action);
            }
        })
        
        constructor() {
            this.cancel = () => this.#cancelled = true;
        }
        
        handler(action) {
            if (this.#cancelled) {
                console.log('Sorry, all actions are «cancelled».');
            } else {
                console.log(`${action} was just executed.`);
            }
        }
    }
    
    const {actions: actionsA, cancel: cancelA} = new Actions();
    
    actionsA.alpha();    // output: alpha was just executed.
    actionsA.foxtrot();  // output: foxtrot was just executed.
    
    cancelA();
    
    actionsA.alpha();    // output: Sorry, all actions are «cancelled».
    actionsA.foxtrot();  // output: Sorry, all actions are «cancelled».
    
    const {actions: actionsB, cancel: cancelB} = new Actions();
    
    actionsB.alpha();    // output: alpha was just executed.
    actionsB.foxtrot();  // output: foxtrot was just executed.
    
    cancelB();
    
    actionsB.alpha();    // output: Sorry, all actions are «cancelled».
    actionsB.foxtrot();  // output: Sorry, all actions are «cancelled».