Skip to content

Commit 438d3ee

Browse files
committed
feat: got it working!
1 parent f5edf9b commit 438d3ee

File tree

6 files changed

+228
-32
lines changed

6 files changed

+228
-32
lines changed
Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Component, effect, signal } from '@angular/core'
2-
import {injectSelector, injectDispatch, injectStore} from "angular-redux";
1+
import { Component } from '@angular/core'
2+
import {injectSelector, injectDispatch} from "angular-redux";
33
import { decrement, increment } from './store/counter-slice'
44
import { RootState } from './store'
55

@@ -16,7 +16,7 @@ import { RootState } from './store'
1616
>
1717
Increment
1818
</button>
19-
<span>{{ count }}</span>
19+
<span>{{ count() }}</span>
2020
<button
2121
aria-label="Decrement value"
2222
(click)="dispatch(decrement())"
@@ -30,22 +30,6 @@ import { RootState } from './store'
3030
export class AppComponent {
3131
count = injectSelector((state: RootState) => state.counter.value)
3232
dispatch = injectDispatch()
33-
34-
store = injectStore()
35-
36-
val = signal(0);
37-
_test = effect(() => {
38-
if (this.val()) {
39-
console.log((this.store.getState() as any).counter.value)
40-
}
41-
})
42-
43-
increment = () => {
44-
setTimeout(() => this.val.set(this.val() + 1), 100)
45-
return increment()
46-
};
47-
decrement = () => {
48-
setTimeout(() => this.val.set(this.val() + 1), 100)
49-
return decrement()
50-
};
33+
increment = increment
34+
decrement = decrement
5135
}

projects/angular-redux/src/lib/inject-selector.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EqualityFn } from './types'
2-
import { inject } from '@angular/core'
2+
import { effect, inject, Signal, signal } from '@angular/core'
33
import { ReduxProvider } from './provider'
44

55
export interface UseSelectorOptions<Selected = unknown> {
@@ -12,7 +12,7 @@ const refEquality: EqualityFn<any> = (a, b) => a === b
1212
export function injectSelector<TState = unknown, Selected = unknown>(
1313
selector: (state: TState) => Selected,
1414
equalityFnOrOptions?: EqualityFn<Selected> | UseSelectorOptions<Selected>,
15-
): Selected {
15+
): Signal<Selected> {
1616
const reduxContext = inject(ReduxProvider);
1717

1818
// const { equalityFn = refEquality } =
@@ -24,7 +24,17 @@ export function injectSelector<TState = unknown, Selected = unknown>(
2424
store
2525
} = reduxContext
2626

27-
const selectedState = selector(store.getState())
27+
const selectedState = signal(selector(store.getState()))
28+
29+
effect((onCleanup) => {
30+
const unsubscribe = store.subscribe(() => {
31+
selectedState.set(selector(store.getState()))
32+
})
33+
34+
onCleanup(() => {
35+
unsubscribe()
36+
})
37+
})
2838

2939
return selectedState
3040
}

projects/angular-redux/src/lib/provide-redux.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Action, Store, UnknownAction } from 'redux'
2-
import { ReduxProvider } from './provider'
2+
import { createReduxProvider, ReduxProvider } from './provider'
33

44
export interface ProviderProps<
55
A extends Action<string> = UnknownAction,
@@ -16,10 +16,6 @@ export function provideRedux<A extends Action<string> = UnknownAction, S = unkno
1616
}: ProviderProps<A, S>) {
1717
return {
1818
provide: ReduxProvider,
19-
useValue: (() => {
20-
const provider = new ReduxProvider<A, S>();
21-
provider.store = store;
22-
return provider;
23-
})()
19+
useValue: createReduxProvider(store)
2420
}
2521
}
Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
1-
import { Injectable } from '@angular/core'
1+
import { Injectable, OnDestroy } from '@angular/core'
22
import type { Action, Store, UnknownAction } from 'redux'
3+
import { createSubscription } from './utils/Subscription'
34

45
@Injectable({providedIn: null})
5-
export class ReduxProvider<A extends Action<string> = UnknownAction, S = unknown> {
6+
export class ReduxProvider<A extends Action<string> = UnknownAction, S = unknown> implements OnDestroy {
67
store!: Store<S, A>;
8+
subscription!: ReturnType<typeof createSubscription>
9+
10+
ngOnDestroy() {
11+
this.subscription.tryUnsubscribe()
12+
this.subscription.onStateChange = undefined
13+
}
14+
}
15+
16+
// TODO: Ideally this runs in the constructor, but DI doesn't allow us to pass items to the constructor?
17+
export function createReduxProvider<A extends Action<string> = UnknownAction, S = unknown>(store: Store<S, A>) {
18+
const provider = new ReduxProvider<A, S>()
19+
provider.store = store
20+
const subscription = createSubscription(store)
21+
provider.subscription = subscription
22+
subscription.onStateChange = subscription.notifyNestedSubs
23+
subscription.trySubscribe()
24+
25+
return provider
726
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { defaultNoopBatch as batch } from './batch'
2+
3+
// encapsulates the subscription logic for connecting a component to the redux store, as
4+
// well as nesting subscriptions of descendant components, so that we can ensure the
5+
// ancestor components re-render before descendants
6+
7+
type VoidFunc = () => void
8+
9+
type Listener = {
10+
callback: VoidFunc
11+
next: Listener | null
12+
prev: Listener | null
13+
}
14+
15+
function createListenerCollection() {
16+
let first: Listener | null = null
17+
let last: Listener | null = null
18+
19+
return {
20+
clear() {
21+
first = null
22+
last = null
23+
},
24+
25+
notify() {
26+
batch(() => {
27+
let listener = first
28+
while (listener) {
29+
listener.callback()
30+
listener = listener.next
31+
}
32+
})
33+
},
34+
35+
get() {
36+
const listeners: Listener[] = []
37+
let listener = first
38+
while (listener) {
39+
listeners.push(listener)
40+
listener = listener.next
41+
}
42+
return listeners
43+
},
44+
45+
subscribe(callback: () => void) {
46+
let isSubscribed = true
47+
48+
const listener: Listener = (last = {
49+
callback,
50+
next: null,
51+
prev: last,
52+
})
53+
54+
if (listener.prev) {
55+
listener.prev.next = listener
56+
} else {
57+
first = listener
58+
}
59+
60+
return function unsubscribe() {
61+
if (!isSubscribed || first === null) return
62+
isSubscribed = false
63+
64+
if (listener.next) {
65+
listener.next.prev = listener.prev
66+
} else {
67+
last = listener.prev
68+
}
69+
if (listener.prev) {
70+
listener.prev.next = listener.next
71+
} else {
72+
first = listener.next
73+
}
74+
}
75+
},
76+
}
77+
}
78+
79+
type ListenerCollection = ReturnType<typeof createListenerCollection>
80+
81+
export interface Subscription {
82+
addNestedSub: (listener: VoidFunc) => VoidFunc
83+
notifyNestedSubs: VoidFunc
84+
handleChangeWrapper: VoidFunc
85+
isSubscribed: () => boolean
86+
onStateChange?: VoidFunc | null
87+
trySubscribe: VoidFunc
88+
tryUnsubscribe: VoidFunc
89+
getListeners: () => ListenerCollection
90+
}
91+
92+
const nullListeners = {
93+
notify() {},
94+
get: () => [],
95+
} as unknown as ListenerCollection
96+
97+
export function createSubscription(store: any, parentSub?: Subscription) {
98+
let unsubscribe: VoidFunc | undefined
99+
let listeners: ListenerCollection = nullListeners
100+
101+
// Reasons to keep the subscription active
102+
let subscriptionsAmount = 0
103+
104+
// Is this specific subscription subscribed (or only nested ones?)
105+
let selfSubscribed = false
106+
107+
function addNestedSub(listener: () => void) {
108+
trySubscribe()
109+
110+
const cleanupListener = listeners.subscribe(listener)
111+
112+
// cleanup nested sub
113+
let removed = false
114+
return () => {
115+
if (!removed) {
116+
removed = true
117+
cleanupListener()
118+
tryUnsubscribe()
119+
}
120+
}
121+
}
122+
123+
function notifyNestedSubs() {
124+
listeners.notify()
125+
}
126+
127+
function handleChangeWrapper() {
128+
if (subscription.onStateChange) {
129+
subscription.onStateChange()
130+
}
131+
}
132+
133+
function isSubscribed() {
134+
return selfSubscribed
135+
}
136+
137+
function trySubscribe() {
138+
subscriptionsAmount++
139+
if (!unsubscribe) {
140+
unsubscribe = parentSub
141+
? parentSub.addNestedSub(handleChangeWrapper)
142+
: store.subscribe(handleChangeWrapper)
143+
144+
listeners = createListenerCollection()
145+
}
146+
}
147+
148+
function tryUnsubscribe() {
149+
subscriptionsAmount--
150+
if (unsubscribe && subscriptionsAmount === 0) {
151+
unsubscribe()
152+
unsubscribe = undefined
153+
listeners.clear()
154+
listeners = nullListeners
155+
}
156+
}
157+
158+
function trySubscribeSelf() {
159+
if (!selfSubscribed) {
160+
selfSubscribed = true
161+
trySubscribe()
162+
}
163+
}
164+
165+
function tryUnsubscribeSelf() {
166+
if (selfSubscribed) {
167+
selfSubscribed = false
168+
tryUnsubscribe()
169+
}
170+
}
171+
172+
const subscription: Subscription = {
173+
addNestedSub,
174+
notifyNestedSubs,
175+
handleChangeWrapper,
176+
isSubscribed,
177+
trySubscribe: trySubscribeSelf,
178+
tryUnsubscribe: tryUnsubscribeSelf,
179+
getListeners: () => listeners,
180+
}
181+
182+
return subscription
183+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Default to a dummy "batch" implementation that just runs the callback
2+
export function defaultNoopBatch(callback: () => void) {
3+
callback()
4+
}

0 commit comments

Comments
 (0)