mirror of
https://github.com/dergigi/boris.git
synced 2025-12-27 19:44:40 +01:00
- Add project structure with TypeScript, React, and Vite - Implement nostr authentication using browser extension (NIP-07) - Add NIP-51 compliant bookmark fetching and display - Create minimal UI with login and bookmark components - Integrate applesauce-core and applesauce-react libraries - Add responsive styling with dark/light mode support - Include comprehensive README with setup instructions This is a minimal MVP for a nostr bookmark client that allows users to view their bookmarks according to NIP-51 specification.
112 lines
2.8 KiB
TypeScript
112 lines
2.8 KiB
TypeScript
import { useDebugValue, useEffect, useRef, useState } from "react";
|
|
import { Observable } from "rxjs";
|
|
import { useForceUpdate, useIsomorphicLayoutEffect } from "./helpers";
|
|
|
|
/**
|
|
* Optimized for safely getting synchronous values from hot or pure observables
|
|
* without triggering extra initial re-rendering.
|
|
*
|
|
* ⚠ If the observable is cold and with side effects
|
|
* they will be performed at least twice!
|
|
*
|
|
* By default this hook will subscribe to the observable at least twice.
|
|
* The first time is for getting synchronous value to prevent extra initial re-rendering.
|
|
* In concurrent mode this may happen more than one time.
|
|
*
|
|
* @template TState State.
|
|
*
|
|
* @param state$ An observable of state value.
|
|
*/
|
|
export function useObservableEagerState<TState>(
|
|
state$: Observable<TState>
|
|
): TState {
|
|
const forceUpdate = useForceUpdate();
|
|
|
|
const state$Ref = useRef(state$);
|
|
const errorRef = useRef<Error | null>();
|
|
const isAsyncEmissionRef = useRef(false);
|
|
|
|
const didSyncEmit = useRef(false);
|
|
|
|
const [state, setState] = useState<TState>(() => {
|
|
let state: TState;
|
|
state$
|
|
.subscribe({
|
|
next: value => {
|
|
didSyncEmit.current = true;
|
|
state = value;
|
|
},
|
|
error: error => {
|
|
errorRef.current = error;
|
|
},
|
|
})
|
|
.unsubscribe();
|
|
return state!;
|
|
});
|
|
|
|
// update the latest observable
|
|
// synchronously after render being committed
|
|
useIsomorphicLayoutEffect(() => {
|
|
state$Ref.current = state$;
|
|
});
|
|
|
|
useEffect(() => {
|
|
errorRef.current = null;
|
|
|
|
// keep in closure for checking staleness
|
|
const input$ = state$Ref.current;
|
|
|
|
let secondInitialValue = state;
|
|
|
|
const subscription = input$.subscribe({
|
|
next: value => {
|
|
if (input$ !== state$Ref.current) {
|
|
// stale observable
|
|
return;
|
|
}
|
|
if (isAsyncEmissionRef.current) {
|
|
// ignore synchronous value
|
|
// prevent initial re-rendering
|
|
setState(() => value);
|
|
} else {
|
|
secondInitialValue = value;
|
|
}
|
|
},
|
|
error: error => {
|
|
if (input$ !== state$Ref.current) {
|
|
// stale observable
|
|
return;
|
|
}
|
|
errorRef.current = error;
|
|
forceUpdate();
|
|
},
|
|
});
|
|
|
|
if (!isAsyncEmissionRef.current) {
|
|
// fix #86 where sync emission may happen before useEffect
|
|
if (secondInitialValue !== state) {
|
|
setState(() => secondInitialValue);
|
|
}
|
|
}
|
|
|
|
isAsyncEmissionRef.current = true;
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
};
|
|
}, [state$]);
|
|
|
|
if (errorRef.current) {
|
|
// Let error boundary catch the error
|
|
throw errorRef.current;
|
|
}
|
|
|
|
if (didSyncEmit.current) {
|
|
useDebugValue(state);
|
|
|
|
return state;
|
|
} else {
|
|
throw new Error("Observable did not synchronously emit a value.");
|
|
}
|
|
}
|