Easy-peasy: Typescript decorators

Created on 11 Nov 2019  路  14Comments  路  Source: ctrlplusb/easy-peasy

Easy-peasy provides some nice abstractions over redux boilerplate but I think Typescript usage can be varied with the help of decorators. During my current project, I created a basic decorator implementation which simply collects some metada from class models and creates an easy-peasy store. I have yet to find any opportunity to use easy-peasy extensively since it takes a rather humble part of my project, still it did great for simple use cases with decorators. A full implementation -if possible- could be very useful.

Edit: Of course there are might be pitfalls in typings because my implementation is rather shallow.

example.store.ts

import { Computed, createStore, createTypedHooks, Model, Thunk } from "./easy-peasy-decorators";

@Model("counter")
export class CounterModel {
    public val = 0;

    public get nextCount(): Computed<number> {
        return this.val + 1;
    }

    @Thunk
    public async incrementThunk() {
        this.increment();
    }

    public increment() {
        ++this.val;
    }

    public reset() {
        this.val = 0;
    }
}

interface IStoreModel {
    counter: CounterModel;
}

const store = createStore<IStoreModel>();

easy-peasy.decorators.ts

import * as easyPeasy from "easy-peasy";

const model: any = {};
const instances: Record<string, any> = {};
const listeners: Record<string, Record<string, easyPeasy.TargetResolver<any, any>>> = {};
const thunks: Record<string, string[]> = {};
let store: easyPeasy.Store;

export function Model(modelName: string) {
    return (ctor: any) => {
        instances[modelName] = new ctor();
        model[modelName] = {};
        listeners[modelName] = {};
        thunks[modelName] = [];

        addState(modelName);
        addActionsAndComputeds(ctor.name, modelName);
    };
}

function addState(modelName: string) {
    const properties = Object.keys(instances[modelName]);

    properties.forEach(property => {
        const initialVal = instances[modelName][property];

        model[modelName][property] = initialVal;
    });
}

function addActionsAndComputeds(ctorName: string, modelName: string) {
    const prototype = instances[modelName].constructor.prototype;
    const descriptors = Object.getOwnPropertyDescriptors(prototype as object);

    Object.entries(descriptors)
        .filter(([methodName, desc]) => methodName !== "constructor")
        .forEach(([methodName, desc]) => {
            const { value, get } = desc;

            if (listeners[ctorName]?.[methodName]) {
                model[modelName][methodName] = easyPeasy.actionOn(
                    listeners[ctorName][methodName],
                    (state, target) => {
                        value.call(state, target);
                    },
                );
            } else if (thunks[ctorName] && thunks[ctorName].includes(methodName)) {
                model[modelName][methodName] = easyPeasy.thunk((actions, payload, { getState }) => {
                    value.call({ ...getState(), ...actions }, payload);
                });
            } else if (value) {
                model[modelName][methodName] = easyPeasy.action((state, payload) => {
                    value.call(state, payload);
                });
            } else if (get) {
                model[modelName][methodName] = easyPeasy.computed(state => {
                    return get.call(state);
                });
            }
        });
}

export function Listener<Model extends object, StoreModel extends object = {}>(
    actionFn: easyPeasy.TargetResolver<ToStoreType<Model>, ToStoreType<StoreModel>>,
) {
    return (ctor: any, methodName: string) => {
        listeners[ctor.constructor.name] = listeners[ctor.constructor.name] || {};
        listeners[ctor.constructor.name][methodName] = actionFn;
    };
}

export function Thunk(ctor: any, methodName: string) {
    thunks[ctor.constructor.name] = thunks[ctor.constructor.name] || [];
    thunks[ctor.constructor.name].push(methodName);
}

type ToStoreType<T extends object> = {
    [P in keyof T]: "computed" extends keyof T[P]
        ? T[P] extends Computed<infer U>
            ? U
            : T[P]
        : T[P] extends (...args: any[]) => any
        ? easyPeasy.Action<T, Parameters<T[P]>[0]>
        : T[P] extends object
        ? ToStoreType<T[P]>
        : T[P];
};

export function createStore<T extends object>() {
    store = easyPeasy.createStore<any>(model);

    return store as easyPeasy.Store<ToStoreType<T>>;
}

export function createTypedHooks<Model extends object>(): {
    useStoreActions: <Result>(
        mapActions: (actions: easyPeasy.Actions<ToStoreType<Model>>) => Result,
    ) => Result;
    useStoreDispatch: () => easyPeasy.Dispatch<ToStoreType<Model>>;
    useStoreState: <Result>(
        mapState: (state: ToStoreType<Model>) => Result,
        dependencies?: any[],
    ) => Result;
} {
    const hooks = easyPeasy.createTypedHooks<any>();

    return hooks as any;
}

export type Computed<T> = T & { computed?: undefined };
docs

All 14 comments

This looks really clean. I wonder is there any progress on this?

Would it be possible to create a 3rd party package that simply wraps the Easy Peasy API, providing this API?

@ctrlplusb I was thinking of adding this to easy-peasy-packages

@ctrlplusb Sure, I can extract the code, add tests and make some refactoring for code quality.

Thanks both. I can create a page for 3rd party packages etc in the website and then link to you. Perhaps it's worth working together on this @CyriacBr and @erencay.

@ctrlplusb Sure. But I'm confused by what @CyriacBr meant by adding. Did you mean simply providing the code in your monorepo as is or implementing an improved version? If you want to publish a full-fledged package I can help you later on.

@erencay I had in mind to publish a full-fledged package which however relies on @easy-peasy/core instead of the react version.
I slightly improved and refactored your code and published it under @easy-peasy/decorators.
You might want to look at my PR.
Any help will be appreciated if you're interested in a standalone version too.

Hmmm, the down side with this approach though @CyriacBr is that the decorators package would not be compatible with easy-peasy.

I'm all for the extended package layers for Vue, Angular, et al. But certainly don't want to overly impact this library.

Needs some more thought from us all. 馃憤

How about we make @easy-peasy/decorators based on easy-peasy instead, with @erencay's help for a full-fledged and improved version, and I'd later on publish @easy-peasy/decorators-standalone for my own needs and the few who prefer a standalone version?
@ctrlplusb

Hi @CyriacBr @erencay

I have created an Easy Peasy Community organisation and have added the both of you to it. You are welcome to create repositories there if you like. No worries if you want to keep them on your own GitHub account either, but might be nice to centralise packages relating to Easy Peasy. I've added you both to the organisation so you will be able to create repo's if needed.

I think it's likely best @erencay create his own repo for the initial decorator implementation. @CyriacBr your repo feels like it's best serving the alternative framework story.

Fine by me. Once I've worked on the VueJS connector I'm going to move the repo to the EP org.

Thanks all. I plan to update the website to publicise this. It'll be with the v3.4.0 updates. 馃憤

FYI, I have just added this to the docs for the v3.4.0 release.

@CyriacBr - when I have completed the plugin architecture refactor it should be way easier to adapt Easy Peasy to other frameworks.

@ctrlplusb Good to know! I got derailed from my initial plan and I'm not sure I'm going to use easy-peasy that way anymore, but I'm not sure yet. I initially wanted to leverage native web components to build framework agnostic libs, and I would have loved to use EP for the native implementation and framework layers.
I'm going to keep an eye on the plugin architecture. If it gets really easy to implement what I had in mind I wouldn't mind pursuing what I started.
Good luck with that!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rankun203 picture rankun203  路  5Comments

vincentjames501 picture vincentjames501  路  6Comments

ifyoumakeit picture ifyoumakeit  路  5Comments

Bazooo picture Bazooo  路  4Comments

24dev picture 24dev  路  5Comments