vuse-rx
website·Indexed 31 days ago
https://vuse-rx.raiondesu.devA 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.
What's inside 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.
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.
Implement flux-like state management with `useRxState`
useRxStateimplements 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 inreactive().The hook returns four key elements:
state: A Vue reactive state object.actions: Functions that trigger the defined reducers to mutate the state.state$: An RxJS observable that emits the state whenever it changes.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();Integrate vuse-rx state with Vue components using syncRef
To connect avuse-rxstate to a Vue component, use the.subscribe()method to retrieve theactions(reducers) and the reactivestate. UsesyncRefto 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), }; }, });Implement separation of concerns for state mutations
By calling a shareduseRxStatehook 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>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 globalvuseRxobject.<script src="https://unpkg.com/vuse-rx"></script> <script> const { useRxState, syncRef } = vuseRx; </script>Install vuse-rx via NPM or Yarn
Install thevuse-rxpackage using npm or yarn to use its reactive bridge utilities in a Vue project.npm i vuse-rx # or yarn add vuse-rxCreate 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 touseRxState. If a factory function is passed instead, the state will be local to the instance. You can use theStatetype 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>;Define stateful and stateless reducers in `useRxState`
Reducers in
useRxStateare 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
stateand an optionalmutationcontext. - Stateless Reducers: Use only their input parameters to compute the mutation. They return the mutation object directly.
The
mutationcontext 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') });- Stateful Reducers: Use the current state to compute the next mutation. They return a function that accepts the current
Customize builtin types for deepReplaceBuiltin strategy
When using thedeepReplaceBuiltinmutation strategy, you can define which classes are treated as 'builtins' (replaced rather than merged) using thestrategyContextoption inuseRxState.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] });Implement a shallow mutation strategy
Theshallowstrategy performs a top-level merge of the mutation object into the state using afor..inloop.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; };Implement a deep mutation strategy
Thedeepstrategy recursively merges the mutation into the state, checking if values can be merged deeper usingcanMergeDeep.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; };