Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-rxjs-elements",
"version": "0.0.6",
"version": "0.1.0-0",
"description": "React fragment for RxJS content",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand All @@ -21,7 +21,7 @@
"test": "jest",
"test:watch": "jest --watch",
"test:debug": "node --inspect node_modules/.bin/jest --watch --runInBand",
"np": "npm run clean && npm run build && np && npm run clean"
"np": "npm run clean && npm run build && np --any-branch && npm run clean"
},
"repository": {
"type": "git",
Expand Down
23 changes: 23 additions & 0 deletions src/components/RefSubject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReplaySubject } from "rxjs";


export class RefSubject<T> extends ReplaySubject<T> {
private _value: T | undefined = void 0;
public get value(): T | undefined {
return this._value;
};

constructor() {
// always 1 last value replay
super(1);

// NOTE: compiled library seem to be overriding Subject .next in
// constructor, which corrupts traditional class method override
const _next = this.next;
this.next = function next(value: T) {
this._value = value;
_next.call(this, value);
}
}

}
104 changes: 104 additions & 0 deletions src/components/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { memo, useEffect, useRef, useState } from "react";
import { BehaviorSubject, isObservable, Observable, of } from "rxjs";
import { createElement$ } from "../elements";
import { EMPTY_DEPENDENCIES } from '../shared';
import { RefSubject } from "./RefSubject";

export type BasicNodeType = JSX.Element | string | number;

// Main hook getter
export interface TUseHook$ {
<F>(fn: (p) => F): RefSubject<F>
}

let _globalUseHook: TUseHook$ | undefined = undefined;
export const useHook$: TUseHook$ = (fn) => {
if (_globalUseHook == undefined) {
throw new Error('Using hooks outside component constructor');
};

return _globalUseHook(fn);
}


// NOTE: it's memo-ised and createElement$-ed, and we might need to optimise that
export function createComponent$<P>(fn: (props: Observable<P>) => BasicNodeType | Observable<BasicNodeType>) {
return memo(createElement$((props: P) => {
const props$ = useRef<BehaviorSubject<P>>();
const output$ = useRef<Observable<BasicNodeType>>();
const [output, setOutput] = useState<any>(null);

// Saving hooks and their streams for future calls
const hookFns = useRef<any[]>([]);
const hookStreams = useRef<RefSubject<any>[]>([]);

// Unconditional props$ stream update

// TODO: React.memo doesn't exactly guarantee that the component won't be
// called twice with the same props, it's rather a performance optimisation.
// So we should add a guard for that or a simple distinctUntilChanged if it
// works
if (!props$.current) {
props$.current = new BehaviorSubject<P>(props);
} else {
props$.current.next(props);
}

// Call the component fn once
if (!output$.current) {
// Record all hooks used: they will be called in each render phase and
// their values pushed to respective Subjects
const useHook = fn => {
const hook$ = new RefSubject<any>();
hookFns.current.push(fn);
hookStreams.current.push(hook$);

// TODO: this exposes Subject API to the user, which should be avoided
// in the future and an Observable interface with current value
// field exposed
return hook$;
}

// Calling component fn, preserving prev and next useHook instance
const prevUseHook = _globalUseHook;
_globalUseHook = useHook;
const result = fn(props$.current);
_globalUseHook = prevUseHook;

// Observable-ing the output
output$.current = isObservable(result)
? result
: of(result);
}

// If any hooks were used -- call all of them & push their results
// (we need to call all hooks on each render, in the same order)
if (hookFns.current.length != 0) {
const hookValues = hookFns.current.map(hook => hook());

useEffect(() => {
// NOTE: Deferring hook effects, so that .next don't initiate a chain of
// updates in current component's render phase

// TODO: this might emit same hook results multiple times, might need some
// change distinction

hookValues.forEach((v, i) => {
hookStreams.current[i].next(v);
});
}, hookValues);
}

// Binding output with
useEffect(() => {
const subscription = output$.current!.subscribe(setOutput);

return () => {
props$.current!.complete(); // complete current stream
subscription.unsubscribe(); // unsubscribe from results
}
}, EMPTY_DEPENDENCIES);

return output;
}));
}
4 changes: 4 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './components';
export * from './RefSubject';
export * from './useMount$';
export * from './useUnmount$';
26 changes: 26 additions & 0 deletions src/components/useMount$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";
import { ReplaySubject } from "rxjs";
import { switchMap } from "rxjs/operators";
import { EMPTY_DEPENDENCIES } from '../shared';
import { useHook$ } from "./components";


export function useMount$() {
return useHook$(() => {
const mount = useRef<ReplaySubject<void>>();

if (!mount.current) {
mount.current = new ReplaySubject(1);
}

useEffect(() => {
mount.current!.next(void 0);
return () => {
mount.current!.complete();
};
}, EMPTY_DEPENDENCIES);

return mount.current;
})
.pipe(switchMap(x => x));
}
26 changes: 26 additions & 0 deletions src/components/useUnmount$.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect, useRef } from "react";
import { ReplaySubject } from "rxjs";
import { switchMap } from "rxjs/operators";
import { EMPTY_DEPENDENCIES } from '../shared';
import { useHook$ } from "./components";


export function useUnmount$() {
return useHook$(() => {
const unmount = useRef<ReplaySubject<void>>();

if (!unmount.current) {
unmount.current = new ReplaySubject(1);
}

useEffect(() => {
return () => {
unmount.current!.next(void 0);
unmount.current!.complete();
};
}, EMPTY_DEPENDENCIES);

return unmount.current;
})
.pipe(switchMap(x => x));
}
2 changes: 1 addition & 1 deletion src/elements.ts → src/elements/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComponentClass, createElement, FunctionComponent, useEffect, useState }
import { isObservable, Observable } from 'rxjs';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { $ } from './fragment';
import { useDestroyObservable, useEmptyObject } from './shared';
import { useDestroyObservable, useEmptyObject } from '../shared';

// TODO: handle ref

Expand Down
2 changes: 1 addition & 1 deletion src/fragment.ts → src/elements/fragment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createElement, Fragment, useEffect, useState } from "react";
import { isObservable } from "rxjs";
import { distinctUntilChanged, takeUntil } from "rxjs/operators";
import { useDestroyObservable } from "./shared";
import { useDestroyObservable } from "../shared";

// TODO: add better TS support

Expand Down
3 changes: 3 additions & 0 deletions src/elements/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './elements';
export * from './fragment';
export * from './intrinsic';
File renamed without changes.
3 changes: 1 addition & 2 deletions src/index.ts
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './elements';
export * from './fragment';
export * from './intrinsic';
export * from './components';
2 changes: 1 addition & 1 deletion tests/elements.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import { Observable, of, Subject } from 'rxjs';
import { $input, createElement$ } from '../src/index';
import { $input, createElement$ } from '../src/elements/index';

// TODO: cover errors on Observables

Expand Down
2 changes: 1 addition & 1 deletion tests/fragment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import { Observable, of, Subject } from 'rxjs';
import { $ } from '../src/index';
import { $ } from '../src/elements/index';
import { ErrorBoundary } from './ErrorBoundary';


Expand Down