Skip to content

Commit

Permalink
feat: add support for context overwriting
Browse files Browse the repository at this point in the history
  • Loading branch information
crutchcorn committed Sep 5, 2024
1 parent a790f5f commit f9c07f6
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useStore } from './use-store'
import { createStoreComposition, useStore as useDefaultStore } from './use-store'
import type { Action, Dispatch, UnknownAction } from 'redux'
import type {InjectionKey} from "vue";
import {ContextKey, VueReduxContextValue} from "../provider/context";

/**
* Represents a custom composition that provides a dispatch function
Expand Down Expand Up @@ -48,8 +50,14 @@ export interface UseDispatch<
* @returns {Function} A `useDispatch` composition bound to the specified context.
*/
export function createDispatchComposition<
StateType = unknown,
ActionType extends Action = UnknownAction,
>() {
>(
context?: InjectionKey<VueReduxContextValue<StateType, ActionType> | null> = ContextKey,
) {
const useStore =
context === ContextKey ? useDefaultStore : createStoreComposition(context)

const useDispatch = () => {
const store = useStore()
return store.dispatch
Expand Down
41 changes: 41 additions & 0 deletions packages/vue-redux/src/compositions/use-redux-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {inject} from "vue";
import type {InjectionKey} from "vue";
import {ContextKey, VueReduxContextValue} from "../provider/context";

/**
* Composition factory, which creates a `useReduxContext` hook bound to a given context. This is a low-level
* composition that you should usually not need to call directly.
*
* @param {InjectionKey<VueReduxContextValue | null>} [context=ContextKey] Context passed to your `provide`.
* @returns {Function} A `useReduxContext` composition bound to the specified context.
*/
export function createReduxContextComposition(context = ContextKey) {
return function useReduxContext(): VueReduxContextValue {
const contextValue = inject(context)

if (process.env.NODE_ENV !== 'production' && !contextValue) {
throw new Error(
'could not find react-redux context value; please ensure the component is wrapped in a <Provider>',
)
}

return contextValue!
}
}

/**
* A composition to access the value of the `VueReduxContext`. This is a low-level
* composition that you should usually not need to call directly.
*
* @returns {any} the value of the `VueReduxContext`
*
* @example
*
* import { useReduxContext } from '@reduxjs/vue-redux'
*
* export const CounterComponent = () => {
* const { store } = useReduxContext()
* return <div>{store.getState()}</div>
* }
*/
export const useReduxContext = /*#__PURE__*/ createReduxContextComposition()
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { inject, readonly, ref, toRaw, watch } from 'vue'
import {inject, type InjectionKey, readonly, ref, toRaw, watch} from 'vue'
import { StoreSymbol } from './provide-store'
import type { StoreContext } from './provide-store'
import type { DeepReadonly, Ref, UnwrapNestedRefs , UnwrapRef} from 'vue'
import type { DeepReadonly, Ref, UnwrapRef} from 'vue'
import type { EqualityFn } from './types'
import {ContextKey, VueReduxContextValue} from "../provider/context";
import {
createReduxContextComposition,
useReduxContext as useDefaultReduxContext,
} from './use-redux-context'

export interface UseSelectorOptions<Selected> {
equalityFn?: EqualityFn<Selected>
Expand Down Expand Up @@ -63,23 +68,31 @@ export interface UseSelector<StateType = unknown> {
/**
* Composition factory, which creates a `useSelector` composition bound to a given context.
*
* @param {InjectionKey<VueReduxContextValue>} [context=StoreSymbol] Injection key passed to your `inject`.
* @returns {Function} A `useSelector` composition bound to the specified context.
*/
export function createSelectorComposition(): UseSelector {
export function createSelectorComposition(
context?: InjectionKey<VueReduxContextValue<any, any> | null> = ContextKey,
): UseSelector {
const useReduxContext =
context === ContextKey
? useDefaultReduxContext
: createReduxContextComposition(context)

const useSelector = <TState, Selected>(
selector: (state: TState) => Selected,
equalityFnOrOptions:
| EqualityFn<Selected>
| UseSelectorOptions<Selected> = {},
): Readonly<Ref<DeepReadonly<UnwrapRef<Selected>>>> => {
const reduxContext = inject(StoreSymbol) as StoreContext

const { equalityFn = refEquality } =
typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
: equalityFnOrOptions

const { store, subscription } = reduxContext
const { store, subscription } = useReduxContext()

// TODO: Introduce wrappedSelector for debuggability

const selectedState = ref(selector(store.getState() as TState))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { inject } from 'vue'
import { StoreSymbol } from './provide-store'
import type {StoreContext} from './provide-store';
import {inject} from 'vue'
import type {InjectionKey} from 'vue'
import type { Action, Store } from 'redux'
import {ContextKey, VueReduxContextValue} from "../provider/context";
import {
createReduxContextComposition,
useReduxContext as useDefaultReduxContext,
} from './use-redux-context'

/**
* Represents a type that extracts the action type from a given Redux store.
Expand Down Expand Up @@ -67,15 +71,22 @@ export interface UseStore<StoreType extends Store> {
/**
* Composition factory, which creates a `useStore` composition bound to a given context.
*
* @param {InjectionKey<VueReduxContextValue>} [context=StoreSymbol] Injection key passed to your `inject`.
* @returns {Function} A `useStore` composition bound to the specified context.
*/
export function createStoreComposition<
StateType = unknown,
ActionType extends Action = Action,
>() {
>(
context?: InjectionKey<VueReduxContextValue<StateType, ActionType> | null> = ContextKey,
) {
const useReduxContext =
context === ContextKey
? useDefaultReduxContext
: // @ts-ignore
createReduxContextComposition(context)
const useStore = () => {
const context = inject(StoreSymbol) as StoreContext
const { store } = context
const { store } = useReduxContext()
return store
}

Expand Down
9 changes: 5 additions & 4 deletions packages/vue-redux/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './provide-store'
export * from './use-store'
export * from './use-dispatch'
export * from './use-selector'
export * from './provider/provider'
export * from './provider/context'
export * from './compositions/use-store'
export * from './compositions/use-dispatch'
export * from './compositions/use-selector'
57 changes: 0 additions & 57 deletions packages/vue-redux/src/provide-store.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/vue-redux/src/provider/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Action, Store, UnknownAction } from 'redux'
import {Subscription} from '../utils/Subscription'
import type { ProviderProps } from './Provider'
import {InjectionKey} from "vue";

export interface VueReduxContextValue<
SS = any,
A extends Action<string> = UnknownAction,
> extends Pick<ProviderProps, 'stabilityCheck' | 'identityFunctionCheck'> {
store: Store<SS, A>
subscription: Subscription
}

export const ContextKey = Symbol.for(`react-redux-context`) as InjectionKey<VueReduxContextValue>
65 changes: 65 additions & 0 deletions packages/vue-redux/src/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { onScopeDispose, provide } from 'vue'
import type { App, InjectionKey } from 'vue'
import type {Action, Store, UnknownAction} from 'redux'
import {ContextKey, ProviderProps, VueReduxContextValue} from "./context";
import {createSubscription, Subscription} from "../utils/Subscription";

export interface ProviderProps<
A extends Action<string> = UnknownAction,
S = unknown,
> {
/**
* The single Redux store in your application.
*/
store: Store<S, A>
/**
* Optional context to be used internally in vue-redux. Use `Symbol() as InjectionKey<VueReduxContextValue<S, A>>` to create a context to be used.
* Set the initial value to null, and the compositions will error
* if this is not overwritten by `provide`.
*
* @see https://vuejs.org/guide/typescript/composition-api#typing-provide-inject
*/
context?: InjectionKey<VueReduxContextValue<S, A> | null>
}


export function getContext<
A extends Action<string> = UnknownAction,
S = unknown,
>({ store }: Pick<ProviderProps<A, S>, "store">): VueReduxContextValue<S, A> | null {
const subscription = createSubscription(store) as Subscription
subscription.onStateChange = subscription.notifyNestedSubs
subscription.trySubscribe()

return {
store,
subscription,
}
}

export function provideStore<
A extends Action<string> = UnknownAction,
S = unknown,
>({store, context}: ProviderProps<A, S>) {
const contextValue = getContext({store})

onScopeDispose(() => {
contextValue.subscription.tryUnsubscribe()
contextValue.subscription.onStateChange = undefined
})

const providerKey = context || ContextKey;

provide(providerKey, contextValue)
}

export function provideStoreToApp<
A extends Action<string> = UnknownAction,
S = unknown,
>(app: App, {store, context}: ProviderProps<A, S>) {
const contextValue = getContext({store})

const providerKey = context || ContextKey;

app.provide(providerKey, contextValue)
}

0 comments on commit f9c07f6

Please sign in to comment.