Skip to content

Commit

Permalink
add RxRef module & hook
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Sep 22, 2023
1 parent dc83c71 commit 6c20898
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-dogs-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect-rx/rx-react": patch
---

add RxRef module & hook
11 changes: 11 additions & 0 deletions docs/rx-react/index.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Added in v1.0.0
- [hooks](#hooks)
- [useRefreshRx](#userefreshrx)
- [useRx](#userx)
- [useRxRef](#userxref)
- [useRxSuspense](#userxsuspense)
- [useRxSuspenseSuccess](#userxsuspensesuccess)
- [useRxValue](#userxvalue)
Expand Down Expand Up @@ -60,6 +61,16 @@ export declare const useRx: <R, W>(
Added in v1.0.0
## useRxRef
**Signature**
```ts
export declare const useRxRef: <A>(ref: RxRef.ReadonlyRef<A>) => A
```
Added in v1.0.0
## useRxSuspense
**Signature**
Expand Down
115 changes: 115 additions & 0 deletions docs/rx/RxRef.ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: RxRef.ts
nav_order: 4
parent: "@effect-rx/rx"
---

## RxRef overview

Added in v1.0.0

---

<h2 class="text-delta">Table of contents</h2>

- [constructors](#constructors)
- [collection](#collection)
- [make](#make)
- [models](#models)
- [Collection (interface)](#collection-interface)
- [ReadonlyRef (interface)](#readonlyref-interface)
- [RxRef (interface)](#rxref-interface)
- [type ids](#type-ids)
- [TypeId](#typeid)
- [TypeId (type alias)](#typeid-type-alias)

---

# constructors

## collection

**Signature**

```ts
export declare const collection: <A>(items: Iterable<A>) => Collection<A>
```
Added in v1.0.0
## make
**Signature**
```ts
export declare const make: <A>(value: A) => RxRef<A>
```
Added in v1.0.0
# models
## Collection (interface)
**Signature**
```ts
export interface Collection<A> extends ReadonlyRef<ReadonlyArray<RxRef<A>>> {
readonly push: (item: A) => Collection<A>
readonly insertAt: (index: number, item: A) => Collection<A>
readonly remove: (ref: RxRef<A>) => Collection<A>
}
```

Added in v1.0.0

## ReadonlyRef (interface)

**Signature**

```ts
export interface ReadonlyRef<A> {
readonly [TypeId]: TypeId
readonly key: string
readonly value: A
readonly subscribe: (f: (a: A) => void) => () => void
readonly map: <B>(f: (a: A) => B) => ReadonlyRef<B>
}
```

Added in v1.0.0

## RxRef (interface)

**Signature**

```ts
export interface RxRef<A> extends ReadonlyRef<A> {
readonly set: (value: A) => RxRef<A>
readonly update: (f: (value: A) => A) => RxRef<A>
}
```

Added in v1.0.0

# type ids

## TypeId

**Signature**

```ts
export declare const TypeId: typeof TypeId
```
Added in v1.0.0
## TypeId (type alias)
**Signature**
```ts
export type TypeId = typeof TypeId
```
Added in v1.0.0
12 changes: 12 additions & 0 deletions packages/rx-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import * as Registry from "@effect-rx/rx/Registry"
import * as Result from "@effect-rx/rx/Result"
import * as Rx from "@effect-rx/rx/Rx"
import type * as RxRef from "@effect-rx/rx/RxRef"
import { globalValue } from "@effect/data/GlobalValue"
import * as Cause from "@effect/io/Cause"
import * as React from "react"

export * as Registry from "@effect-rx/rx/Registry"
export * as Result from "@effect-rx/rx/Result"
export * as Rx from "@effect-rx/rx/Rx"
export * as RxRef from "@effect-rx/rx/RxRef"

/**
* @since 1.0.0
Expand Down Expand Up @@ -194,3 +196,13 @@ export const useRxSuspenseSuccess = <E, A>(
value: result.value.value
}
}

/**
* @since 1.0.0
* @category hooks
*/
export const useRxRef = <A>(ref: RxRef.ReadonlyRef<A>): A => {
const [value, setValue] = React.useState(ref.value)
React.useEffect(() => ref.subscribe(setValue), [ref])
return value
}
192 changes: 192 additions & 0 deletions packages/rx/src/RxRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* @since 1.0.0
*/
import * as Equal from "@effect/data/Equal"
import { globalValue } from "@effect/data/GlobalValue"

/**
* @since 1.0.0
* @category type ids
*/
export const TypeId = Symbol.for("@effect-rx/rx/RxRef")

/**
* @since 1.0.0
* @category type ids
*/
export type TypeId = typeof TypeId

/**
* @since 1.0.0
* @category models
*/
export interface ReadonlyRef<A> {
readonly [TypeId]: TypeId
readonly key: string
readonly value: A
readonly subscribe: (f: (a: A) => void) => () => void
readonly map: <B>(f: (a: A) => B) => ReadonlyRef<B>
}

/**
* @since 1.0.0
* @category models
*/
export interface RxRef<A> extends ReadonlyRef<A> {
readonly set: (value: A) => RxRef<A>
readonly update: (f: (value: A) => A) => RxRef<A>
}

/**
* @since 1.0.0
* @category models
*/
export interface Collection<A> extends ReadonlyRef<ReadonlyArray<RxRef<A>>> {
readonly push: (item: A) => Collection<A>
readonly insertAt: (index: number, item: A) => Collection<A>
readonly remove: (ref: RxRef<A>) => Collection<A>
}

/**
* @since 1.0.0
* @category constructors
*/
export const make = <A>(value: A): RxRef<A> => new RxRefImpl(value)

/**
* @since 1.0.0
* @category constructors
*/
export const collection = <A>(items: Iterable<A>): Collection<A> => new CollectionImpl(items)

class Subscribable<A> {
listeners: Array<(a: A) => void> = []
listenerCount = 0

notify(a: A) {
for (let i = 0; i < this.listenerCount; i++) {
this.listeners[i](a)
}
}

subscribe(f: (a: A) => void): () => void {
this.listeners.push(f)
this.listenerCount++

return () => {
const index = this.listeners.indexOf(f)
if (index !== -1) {
this.listeners[index] = this.listeners[this.listenerCount - 1]
this.listeners.pop()
this.listenerCount--
}
}
}
}

const keyState = globalValue("@effect-rx/rx/RxRef/keyState", () => ({
count: 0,
generate() {
return `RxRef-${this.count++}`
}
}))

class ReadonlyRefImpl<A> extends Subscribable<A> implements ReadonlyRef<A> {
readonly [TypeId]: TypeId
readonly key = keyState.generate()
constructor(public value: A) {
super()
this[TypeId] = TypeId
}
map<B>(f: (a: A) => B): ReadonlyRef<B> {
return new MapRefImpl(this, f)
}
}

class RxRefImpl<A> extends ReadonlyRefImpl<A> implements RxRef<A> {
set(value: A) {
if (Equal.equals(value, this.value)) {
return this
}
this.value = value
this.notify(value)
return this
}

update(f: (value: A) => A) {
return this.set(f(this.value))
}
}

class MapRefImpl<A, B> implements ReadonlyRef<B> {
readonly [TypeId]: TypeId
readonly key = keyState.generate()
constructor(readonly parent: ReadonlyRef<A>, readonly transform: (a: A) => B) {
this[TypeId] = TypeId
}
get value() {
return this.transform(this.parent.value)
}
subscribe(f: (a: B) => void): () => void {
let previous = this.transform(this.parent.value)
return this.parent.subscribe((a) => {
const next = this.transform(a)
if (Equal.equals(next, previous)) {
return
}
previous = next
f(next)
})
}
map<C>(f: (a: B) => C): ReadonlyRef<C> {
return new MapRefImpl(this, f)
}
}

class CollectionImpl<A> extends ReadonlyRefImpl<Array<RxRef<A>>> implements Collection<A> {
constructor(items: Iterable<A>) {
super([])
for (const item of items) {
this.value.push(this.makeRef(item))
}
}

makeRef(value: A) {
const ref = new RxRefImpl(value)
const notify = (value: A) => {
ref.notify(value)
this.notify(this.value)
}
return new Proxy(ref, {
get(target, p, _receiver) {
if (p === "notify") {
return notify
}
return target[p as keyof RxRef<A>]
}
})
}

push(item: A) {
const ref = this.makeRef(item)
this.value.push(ref)
this.notify(this.value)
return this
}

insertAt(index: number, item: A) {
const ref = this.makeRef(item)
this.value.splice(index, 0, ref)
this.notify(this.value)
return this
}

remove(ref: RxRef<A>) {
const index = this.value.indexOf(ref)
if (index !== -1) {
this.value.splice(index, 1)
this.notify(this.value)
}
return this
}
}
Loading

0 comments on commit 6c20898

Please sign in to comment.