ManuelSch / vuex-rxjs

A type-safe Vuex alternative utilizing RxJS; Compatible with Vue Devtools
MIT License
11 stars 0 forks source link

vuex-rxjs 💽🔄👁

State management for Vue:

Installation ⬇

Install RxJS as a peer dependency: npm install rxjs --save

npm install vuex-rxjs --save
yarn add vuex-rxjs

Usage (the simple way) 👶

First, make sure VuexRxJS is properly installed/registered:

main.ts:

import {VuexRxJS} from "vuex-rxjs";

VuexRxJS(Vue);  // Install VuexRxJS

new Vue({
    render: h => h(App),
}).$mount('#app')

Then, define your application state and your first store:

store.ts:

import {Store} from "vuex-rxjs/dist/store";

/**
 * Define the state:
 */
class State {
    counter: number = 0;    // initial value for counter
}

/**
 * Define your store:
 */
class SimpleStore extends Store<State> {

    /**
     * Mutation:
     */
    increment() {
        this.commit({
            type: 'INCREMENT_COUNTER',
            payload: state => ({
                counter: state.counter + 1
            })
        });

        // NOTE: the payload function never alters the state;
        // instead, it returns a new state object
    }
}

/**
 * Instantiate your store and export it:
 */
export const store = new SimpleStore(new State());

Now you can use your store in all of your components:

Component.vue:

<template>
    <div id="app">
        <p>Count: {{count}}</p>
        <button @click="addOne()">Add 1</button>
    </div>
</template>

<script lang="ts">
    import {Component, Vue} from 'vue-property-decorator';
    import {store} from "./store";

    @Component
    export default class App extends Vue {

        // Bind a state property to a component property:
        count: number = store.bind(state => state.counter);

        // Call a store method:
        addOne() {
            store.increment();
        }
    }
</script>

🍻🎊 Done! 🎉🥂

Modules (advanced usage) 💪

In complex applications, you will probably want to split your state into different parts, like so:

Rootstate
└ auth
└ profile
└ article
└ ... 

Modules help you do exactly this. First, define your application state (root state):

root.store.ts:

/**
 * Define the root state:
 */
class RootState {
    auth = new AuthState();
    profile = new ProfileState();
    article = new ArticleState();
}

export const rootStore = new Store(new RootState());

profile.store.ts:

/**
 * Define the profile state:
 */
class ProfileState {
    username: string = '';
}

/**
 * Define your module store:
 */
class ProfileStore extends Module<ProfileState, RootState> {

    setUsername(newName: string) {
        this.commit({
            type: 'SET_USERNAME',
            payload: profile => ({
                username: newName
            })
        });
    }
}

export const profileStore = new ProfileStore(
    rootStore,
    rootState => rootState.profile,                         // mapping from the root state to the profile state
    (profile, rootState) => rootState.profile = profile     // mapping from the profile state to the root state
);

Component.vue:

<template>
    <div id="app">
        <p>Your name: {{username}}</p>
        <button @click="changeName()">I'm Bob now!</button>
    </div>
</template>

<script lang="ts">
    import {profileStore} from "./profile.store";

    @Component
    export default class App extends Vue {

        username: string = profileStore.bind(profile => profile.username);

        changeName() {
            profileStore.setUsername('Bob');
        }
    }
</script>

API and types

Store:

class Store<RootState> {

    constructor(initialState: RootState);

    // in case you need to directly subscribe to the state:
    state$: Observable<RootState>;

    // bind a state property to a component property:
    bind<PropertyType>(mapping: (state: RootState) => PropertyType): PropertyType;

    // mutate the state:
    commit(mutation: Mutation<RootState>): void;

    // get the current state and invoke mutations (or do other stuff) based on it:
    dispatch(action: Action<RootState>): Promise<void>;
}

Module:

class Module<ModuleState, RootState> {

    constructor(
        parentStore: IStore<RootState>,
        stateMap: (state: RootState) => ModuleState,
        dispatchMap: (moduleState: ModuleState, rootState: RootState) => void
    );

    // in case you need to directly subscribe to the module state:
    state$: Observable<ModuleState>;

    // bind a module state property to a component property:
    bind<PropertyType>(mapping: (state: ModuleState) => PropertyType): PropertyType;

    // mutate the module state:
    commit(mutation: Mutation<ModuleState, RootState>): void;

    // get the current module state and invoke mutations (or do other stuff) based on it:
    dispatch(action: Action<ModuleState, RootState>): Promise<void>;
}

Mutations and Actions:

interface Mutation<State, RootState=State> {
    type: string;
    payload: Payload<State, RootState>;
}

type Payload<State, RootState=State> = (state: State, rootState: RootState) => State;

type Action<State, RootState=State> = (state: State, rootState: RootState) => Promise<void>;

Examples 👀

Todo 🗒