vuse-rx

website·Indexed 31 days ago

https://vuse-rx.raiondesu.dev

A bridge library that connects Vue's reactive states and refs with RxJS observables and subjects. It provides utilities for interoperability between RxJS and Vue's reactivity system, including the useRxState hook for flux-like state management, lifecycle hook utilities like fromHook and useSubscription, and operators such as setRef and mapError.

Tokens
5.8K
Snippets
29
Records
38
Agent score
53%

What's inside vuse-rx

  1. Overview of vuse-rx

    vuse-rx is a bridge library designed to connect Vue's reactive states and refs with RxJS observables and subjects. It aims to enforce separation of concerns and reduce boilerplate code while providing first-class TypeScript support.
  2. Overview of vuse-rx for Vue 3 and RxJS integration

    vuse-rx is a bridge library designed to connect Vue 3's reactive states and refs with RxJS observables and subjects. It aims to simplify the use of RxJS in Vue 3 by treating both RxJS observables and Vue reactive objects as equals, allowing for a seamless transition between them.

    Key benefits include:

    • Separation of Concerns: Encourages separating components from state and state from business logic (similar to Flux/Vuex patterns).
    • Reduced Boilerplate: Elevates the abstraction level above manual creation of observables from events and callbacks, allowing for declarative definition of application rules.
    • State Management: Provides built-in state management capabilities that can replace Vuex for projects already utilizing RxJS.
    • TypeScript Support: Offers first-class TypeScript support.
    • Harmony: Allows the use of native Vue APIs alongside RxJS observables without losing control over the resulting streams.
  3. Implement flux-like state management with `useRxState`

    useRxState implements a light flux pattern using RxJS Observables. It is flexible enough to be used as a global singleton or locally within a component. It automatically makes the state reactive for Vue, eliminating the need to manually wrap it in reactive().

    The hook returns four key elements:

    1. state: A Vue reactive state object.
    2. actions: Functions that trigger the defined reducers to mutate the state.
    3. state$: An RxJS observable that emits the state whenever it changes.
    4. actions$: Observables triggered on specific actions.
    import { useRxState } from 'vuse-rx';
    
    const { 
      actions, 
      state, 
      state$, 
      actions$ 
    } = useRxState({ count: 0 })({
      increment: () => state => ({ count: state.count + 1 })
    }).subscribe();
  4. Integrate vuse-rx state with Vue components using syncRef

    To connect a vuse-rx state to a Vue component, use the .subscribe() method to retrieve the actions (reducers) and the reactive state. Use syncRef to create a bidirectional link between a specific state property and a Vue ref, allowing for automatic synchronization and optional type conversion (e.g., converting a number to a string for an input field).
    import { defineComponent, ref, toRef } from 'vue';
    import { syncRef } from 'vuse-rx';
    import { useStopwatch } from './stopwatch.js';
    
    export default defineComponent({
      setup() {
        // Subscribe to get actions and reactive state
        const { actions, state } = useStopwatch().subscribe(state => {
          console.log('state updated: ', state);
        });
    
        return {
          ...actions,
          state,
          // syncRef keeps this ref in sync with state.speed
          // { to: String } ensures the ref holds a string representation
          speedRef: syncRef(toRef(state, 'speed'), { to: String }),
          
          // Example of overriding an action for custom input handling
          setMaxValue: maxRef => actions.setMaxValue(maxRef === '' ? NaN : +maxRef),
        };
      },
    });
  5. Implement separation of concerns for state mutations

    By calling a shared useRxState hook in different components and passing different reducer objects, you can isolate mutation logic. This allows components to share the same state without sharing the same action logic, preventing unrelated code from being piled into a single file.
    <!-- Component 1: Responsible for incrementing -->
    <script setup lang="ts">
    import { useSharedState } from './state';
    
    const { actions, state } = useSharedState({
      increment: () => (state) => ({ count: state.count + 1 }),
    });
    </script>
    
    <!-- Component 2: Responsible for setting specific values -->
    <script setup lang="ts">
    import { useSharedState } from './state';
    
    const { actions, state } = useSharedState({
      setCount: (val: number) => () => ({ count: val }),
    });
    </script>
  6. Use vuse-rx via UMD build

    Include vuse-rx in your project using a script tag from unpkg. The library exports its utilities via the global vuseRx object.
    <script src="https://unpkg.com/vuse-rx"></script>
    <script>
      const { useRxState, syncRef } = vuseRx;
    </script>
  7. Create a shared global state with useRxState

    To create a state that is shared across all modules and components, pass a plain object as the first parameter to useRxState. If a factory function is passed instead, the state will be local to the instance. You can use the State type to automatically infer the type of the shared state.
    import { State, useRxState } from 'vuse-rx';
    
    // Passing a plain object makes this state shared between all users of this hook
    export const useSharedState = useRxState({ count: 0 }); 
    
    // Infer the state type
    export type CounterState = State<typeof useSharedState>;
  8. Define stateful and stateless reducers in `useRxState`

    Reducers in useRxState are pure functions that return a 'Mutation' (a partial state object) to be merged into the original state. There are two types of reducers:

    • Stateful Reducers: Use the current state to compute the next mutation. They return a function that accepts the current state and an optional mutation context.
    • Stateless Reducers: Use only their input parameters to compute the mutation. They return the mutation object directly.

    The mutation context allows you to trigger errors (mutation?.error()) or complete the stream (mutation?.complete()), which can be handled in the .subscribe() block.

    const { actions, state } = useRxState({ count: 0 })({
      // Stateful: depends on current state
      add: (addAmount) => (state, mutation) => {
        if (addAmount < 0) {
          mutation?.error('add amount cannot be negative!');
          return {}; // No change
        }
        return { count: state.count + addAmount };
      },
    
      // Stateless: ignores current state
      set: (newValue) => ({ count: newValue }),
    }).subscribe({
      error: err => console.error(err),
      complete: () => console.log('Finished')
    });
  9. Customize builtin types for deepReplaceBuiltin strategy

    When using the deepReplaceBuiltin mutation strategy, you can define which classes are treated as 'builtins' (replaced rather than merged) using the strategyContext option in useRxState.
    import { useRxState, defaultBuiltin } from 'vuse-rx';
    
    class MyBuiltin {}
    
    // Option 1: Add to default builtins (Array, Date, RegExp, Error + MyBuiltin)
    useRxState(state, { strategyContext: [...defaultBuiltin, MyBuiltin] });
    
    // Option 2: Replace default builtins (Only MyBuiltin is replaced, others are deep-merged)
    useRxState(state, { strategyContext: [MyBuiltin] });
  10. Implement a shallow mutation strategy

    The shallow strategy performs a top-level merge of the mutation object into the state using a for..in loop.
    export type ShallowMutation<S> = {
      [K in keyof Partial<S>]: S[K];
    };
    
    export const shallow = <S extends Record<PropertyKey, any>>(state: S) => 
      (mutation: ShallowMutation<S>) => {
        for (const key in mutation) {
          state[key] = mutation[key];
        }
        return state;
      };
  11. Implement a deep mutation strategy

    The deep strategy recursively merges the mutation into the state, checking if values can be merged deeper using canMergeDeep.
    import { canMergeDeep } from './common';
    
    export type DeepMutation<T> = T extends object
      ? T extends Array<infer U> ? Array<DeepMutation<U>>
      : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepMutation<U>>
      : T extends Record<any, any> ? { [K in keyof Partial<T>]: DeepMutation<T[K]> }
      : Partial<T>
      : T;
    
    export const deep = <S extends Record<PropertyKey, any>>(state: S) => 
      (mutation: DeepMutation<S>) => {
        for (const key in mutation) {
          state[key as keyof S] = canMergeDeep(state, mutation, key)
            ? deep(state[key])(mutation[key])
            : mutation[key] as S[keyof S];
        }
        return state;
      };