Coder Social home page Coder Social logo

crimx / observable-hooks Goto Github PK

View Code? Open in Web Editor NEW
997.0 16.0 43.0 9.47 MB

⚛️☯️💪 React hooks for RxJS Observables. Concurrent mode safe.

Home Page: https://observable-hooks.js.org

License: MIT License

JavaScript 2.48% TypeScript 97.52%
rxjs-hooks rxjs react-rxjs react-rxjs-hooks react-rxjs-observable concurrent-mode rxjs-observables suspense

observable-hooks's Introduction

Docs npm-version Build Status Coverage Status

logo

Concurrent mode compatible React hooks for RxJS Observables. Simple, flexible, testable and performant.

  • Seamless integration of React and RxJS.
    • Concurrent mode compatible.
    • Props, state and context to Observables.
    • Observables to states and props events.
    • Conditional rendering with stream of React Components.
    • Render-as-You-Fetch with React Suspense.
    • No tap hack needed. With Epic-like signature Observable operation is pure and testable.
  • Full-powered RxJS. Do whatever you want with Observables. No limitation nor compromise.
  • Fully tested. We believe in stability first. This project will always maintain a 100% coverage.
  • Tiny and fast. A lot of efforts had been put into improving integration. This library should have zero visible impact on performance.
  • Compatible with RxJS 6 & 7.

Installation

pnpm add observable-hooks

Why?

React added hooks for reusing stateful logic.

Observable is a powerful way to encapsulate both sync and async logic.

Testing Observables is also way easier than testing other async implementations.

With observable-hooks we can create rich reusable Components with ease.

What It Is Not

This library is not for replacing state management tools like Redux but to reduce the need of dumping everything into global state.

Using this library does not mean you have to turn everything observable which is not encouraged. It plays well side by side with other hooks. Use it only on places where it's needed.

At First Glance

import * as React from "react";
import { useObservableState } from "observable-hooks";
import { timer } from "rxjs";
import { switchMap, mapTo, startWith } from "rxjs/operators";

const App = () => {
  const [isTyping, updateIsTyping] = useObservableState(
    transformTypingStatus,
    false
  );

  return (
    <div>
      <input type="text" onKeyDown={updateIsTyping} />
      <p>{isTyping ? "Good you are typing." : "Why stop typing?"}</p>
    </div>
  );
};

// Logic is pure and can be tested like Epic in redux-observable
function transformTypingStatus(event$) {
  return event$.pipe(
    switchMap(() => timer(1000).pipe(mapTo(false), startWith(true)))
  );
}

Usage

Read the docs at https://observable-hooks.js.org or ./docs.

Examples are in here. Play on CodeSandbox:

Note that there are also some useful utilities for common use cases to reduce garbage collection.

Developing

Install dependencies:

pnpm i

Run tests:

pnpm test

Lint code:

pnpm lint

observable-hooks's People

Contributors

boussadjra avatar crimx avatar dependabot[bot] avatar fytriht avatar goohooh avatar highestop avatar hoangbits avatar jbmusso avatar jkhaui avatar kreedzt avatar mvgijssel avatar oliverjash avatar sbley avatar willium avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

observable-hooks's Issues

rfc: 为 useObservable 添加泛型以匹配多种 Observable

当前问题

const state$ = useObservable(() => new BehaviorSubject(''))

state$ 始终被推断为 Observable<string>, 后续使用无法调用诸如: value, getValue() 等属性方法.

提议:

useObservable 的类型声明改为:

export function useObservable<
  TOutput, 
  TReturnType extends Observable<TOutput> = Observable<TOutput>
>(init: () => TReturnType): TReturnType;

这样当使用 Observable 的子类型时能得到正确的推断:

const state$ = useObservable(() => new BehaviorSubject(''))
// typeof state$ === BehaviorSubject<string>

同时依旧能保持原有的泛型返回类型:

const state$ = useObservable<string>(() => new Subject())
// typeof state$ === Observable<string>

补充

也可以使用另一种方法保证类型兼容:

type TOutput<T> = T extends Observable<any> ? T : Observable<T>

export function useObservable<T>(init: () => TOutput<T>): TOutput<T>

const state$ = useObservable(() => new BehaviorSubject(''))
// typeof state$ === BehaviorSubject<string>
const state$ = useObservable<string>(() => new Subject())
// typeof state$ === Observable<string>

Make XHR requests using ObervableResource with a parameter

Hi @crimx,
thanks a lot for this project!

I'm trying Render as you fetch suspense but I'm having issues with ObservableResource when I need to "create them inside components".

Basically I have

const r = new ObservableResource(ajax.get(API_URL));

but sometimes, I'm using routing which is mixed into the API_URL

const { id } = useParams();
const r = new ObservableResource(ajax.get(API_URL + '/' + id));

In this case:

  • if I create my ObservableResource inside the child component (where I useObservableSuspense(r), I get infinite loop (I think I get why)
  • if I create my ObservableResource inside the parent component and pass it to the child component (where I <Suspense...></Suspense>), I get 2 calls (I don't really understand why)

What do you suggest? I couldn't find any example using ajax from rxjs. Do you have any? What do you think overall about the approach?

Thank you for your time.

Execute twice when use useObserveableState & useSubscription together

Steps to reproduce

  1. useObservable
const value$ = useObservable(input$ => input$ |> switchMap([v] => ajax(...)), [props.value])
  1. useObservableState
const value = useObservableState(value$)
  1. useSubscription
useSubscription(value$, v => props.onChange(v))

What is expected?

just request once

what is actually happening?

request twice

Question - descendent re-rendering?

Wasn't sure the best place to ask this - but let's say I have observables A$ and B$ and this tree:

--Parent
----ChildA (reacts to A$)
----ChildB (reacts to B$)

Will ChildA re-render if B$ changes (or vice-versa?)

Basically I'm wondering if I have some global observables, do I still need a solution like Recoil to avoid needless re-renders on components that don't actually use those observables?

I'm also curious how exactly this works if there's no global store or tracking to optimize that :)

Thanks!

Defer creation of observable

As I was migrating from useObservable in rxjs-hooks to the one provided by this library, I ran into an interesting issue. Take this code:

useObservable(() => Rx.of(window.navigator.appName));

This code is part of a component which is rendered on the client and server. Of course, the observable will do nothing on the server, since it will never be subscribed to (effects are never ran in React SSR).

In rxjs-hooks this worked, but when I migrated it to this library I got an error on the server.

ReferenceError: window is not defined

IIUC, in rxjs-hooks the creation of the observable happens during an effect, immediately before subscription: https://github.com/LeetCode-OpenSource/rxjs-hooks/blob/89c4b89265b410a7c46cd5060629637e32c5ce8c/src/use-observable.ts#L37

Whereas in this library, the creation of the observable happens immediately during the render:

const source$Ref = useRefFn(() => init(inputs$Ref.current))

Perhaps we could wrap the init call in defer (from rxjs)? Something like

-  const source$Ref = useRefFn(() => init(inputs$Ref.current))
+  const source$Ref = useRefFn(() => defer(() => init(inputs$Ref.current)))

[Bug]: `useObservable*` hooks does not work when observed value is a `Map` instance

In case a JS Map is used as value for BehaviorSubject the value provided from useObservable* hooks will be stale.

Try:

const subject = new BehaviorSubject(new Map());

// subscription outside React (works as expected - emitted 4 times)
subject.subscribe((map) => {
  console.log('map is:', map.keys());
});

setTimeout(() => {
  subject.next(subject.value.set(1, 1));
  setTimeout(() => {
    subject.next(subject.value.set(2, 2));
    setTimeout(() => {
      subject.next(subject.value.set(3, 3));
    }, 1000);
  }, 1000);
}, 1000);

// define react component
const Component = () => {
  const map = useObservableEagerState(subject);
  console.log('map keys: ', map.keys()); // <-- it will NOT trigger rerender 4 times
  return <></>
}

useObservableEagerState missing emit

I have a Preact app that is using observables for state and use observable-hooks library to update components.

The problem I am facing is with useObservableEagerState missing important emits. I have a specific scenario involving a mouse-over component and backend call-- both of which are asynchronous to the lifecycle, that renders the component with the INITIAL value, but then misses the backend call's data from the second emit of the observable.

I have tracked this down to this line:
https://github.com/crimx/observable-hooks/blob/master/packages/observable-hooks/src/use-observable-eager-state.ts#L65

I can see that my observable is emitting the data within this useEffect-subscription, but it is being ignored because isAsyncEmissionRef.current is false.

I understand that this logic is there to protect against triggering extra initial re-rendering, but if it is "swallowing" a value, then that isn't desirable either.

Shouldn't this function guarantee that the synchronous values not be ignored? Something along the lines of:

  useEffect(() => {
    ...
    const NO_VALUE = Symbol("NO_VALUE");
    let synchronousValue: TState | typeof NO_VALUE = NO_VALUE;
    const subscription = input$.subscribe({
      next: value => {
        ...
        if (isAsyncEmissionRef.current) {
          // ignore synchronous value
          // prevent initial re-rendering
          setState(value)
        } else {
          synchronousValue = value;
        }
      },
      error: error => {
        ...
      }
    })

    if(synchronousValue !== NO_VALUE) {
      setState(synchronousValue);
    }

    isAsyncEmissionRef.current = true

    ...

Using callbacks as refs (`useObservableCallback`)

https://stackblitz.com/edit/react-ts-a3yjha

This example doesn't currently work. The callback is called but I suspect this happens before the observable is subscribed to. I'm not sure how to fix this—we can't subscribe any earlier? Do you have any ideas?

Perhaps we need a way to subscribe to an observable immediately, e.g. using useConstant?

For context, this is required to implement a pattern such as this: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node.

import React from "react";
import { render } from "react-dom";

import { useObservableCallback, useLayoutSubscription } from "observable-hooks";

const Comp = () => {
  const [ref, ref$] = useObservableCallback(ob$ => ob$);

  useLayoutSubscription(ref$, console.log);

  return <input ref={ref} />;
};

render(<Comp />, document.getElementById("root"));

useObservableState: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

I have an observable like below. its using an observable called period (created by useObservable(new BehaviourSubject())). I seem to get this issue, even when I'm not doing anything with the output data.

 const data = useObservableState(
      props.period.subject$.pipe(
         map((period) => ({
            primary: formatAggregationPeriodForDisplay(intl, period)
         }))
      )
   );

period subject isn't used to render anything else, I've commented out everything..

any thoughts on ideas on this? is this a bug?

attached is the call stack

overrideMethod | @ | react_devtools_backend.js:2430
-- | -- | --
  | printWarning | @ | react-dom.development.js?61bb:67
  | error | @ | react-dom.development.js?61bb:43
  | checkForNestedUpdates | @ | react-dom.development.js?61bb:23812
  | scheduleUpdateOnFiber | @ | react-dom.development.js?61bb:21835
  | dispatchAction | @ | react-dom.development.js?61bb:16139
  | next | @ | use-subscription-internal.js?531a:41
  | SafeSubscriber.__tryOrUnsub | @ | Subscriber.js?1453:192
  | SafeSubscriber.next | @ | Subscriber.js?1453:130
  | Subscriber._next | @ | Subscriber.js?1453:76
  | Subscriber.next | @ | Subscriber.js?1453:53
  | MapSubscriber._next | @ | map.js?ebb6:41
  | Subscriber.next | @ | Subscriber.js?1453:53
  | BehaviorSubject._subscribe | @ | BehaviorSubject.js?dba1:22
  | Observable._trySubscribe | @ | Observable.js?e9b9:43
  | Subject._trySubscribe | @ | Subject.js?2bd2:89
  | Observable.subscribe | @ | Observable.js?e9b9:29
  | MapOperator.call | @ | map.js?ebb6:18
  | Observable.subscribe | @ | Observable.js?e9b9:24
  | eval | @ | use-subscription-internal.js?531a:33
  | invokePassiveEffectCreate | @ | react-dom.development.js?61bb:23487
  | callCallback | @ | react-dom.development.js?61bb:3945
  | invokeGuardedCallbackDev | @ | react-dom.development.js?61bb:3994
  | invokeGuardedCallback | @ | react-dom.development.js?61bb:4056
  | flushPassiveEffectsImpl | @ | react-dom.development.js?61bb:23574
  | unstable_runWithPriority | @ | scheduler.development.js?3069:468
  | runWithPriority$1 | @ | react-dom.development.js?61bb:11276
  | flushPassiveEffects | @ | react-dom.development.js?61bb:23447
  | eval | @ | react-dom.development.js?61bb:23324
  | workLoop | @ | scheduler.development.js?3069:417
  | Show 2 more frames


❓ Question: How can i use useObservable to create a subject

Hi! I'm currently learning RxJs and using it with react.

const flag$ = useObservable(pluckFirst, [props.flag])

I was wondering how i can use the above to create a subject? I wanted to do it while staying in react. Do i have to create a separate subject resource thats outside of react?

thanks for the wonderful library

Examples of data transformation

It would be interesting to see some official examples of how data transformation can be achieved with this library.

The examples could show how data from a fetch can be converted (maybe normalized) or how a series of transformations can benefit of observables.

useSubscription closure not updated

Hello!

I have a similar use of useSubscription which access closure (a prop).

const [debug, setDebug] = useState(false)
const subscription = useSubscription(events$, null, error => {
  if (debug) {
    console.log(error)
  }
})

But when the prop is updated, and useSubscription's event handler is fired, it's always the old prop's value. I could add the prop in the observable used by useSubscription but it's cumbersome. Is there another way?

The `BehaviorSubject` of `useObservable` could cause resource leaks

Root cause:

export function useObservableInternal(...): Observable<TOutput> {
  if (!inputs) {
    return useState(init as () => Observable<TOutput>)[0]
  }

  const [inputs$] = useState(() => new BehaviorSubject(inputs))
  const [source$] = useState(() => init(inputs$))

  const firstEffectRef = useRef(true)
  useCustomEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$.next(inputs)
  }, inputs)

  // No `inputs$.complete()` presents :/

  return source$
}

When using operators like shareReplay(), they typically rely on the source observable(i.e. the BehaviorSubject inside useObservable()) to send a complete signal in order to unsubscribe the internal ReplaySubject from it. If no complete signal is received, resource leaks could occur.

Stackblitz URL: https://stackblitz.com/edit/stackblitz-starters-ghtjf1?devToolsHeight=33&file=src%2FApp.tsx

The example above is using RxJS 6. Haven't tested on RxJS 7 yet.

  • In the example, we use a toggle button to control the existence of FantasyGauge.
  • In FantasyGauge we use a modified version of useObservable with myShareReplay (which just logs some actions on top of original shareReplay logics from RxJS 6.2.1).
  • When FantasyGauge is destroyed, the BehaviorSubject of myUseObservable still holds some observers. See BEFORE.mp4 below.
  • When added inputs$.complete(), the BehaviorSubject no longer holds any observers. See AFTER.mp4 below.

I also find that useObservableRef and useObservableCallback both use BehaviorSubject or Subject under the hood, and both of them don't seem to call complete() either. So I suspect these hooks might also be vulnerable to resource leaks.

BEFORE.mp4
AFTER.mp4

useObservableState(BehaviorSubject) gives undefined when first rendering

The title says it all

import { BehaviorSubject } from "rxjs";
import { useObservableState } from "observable-hooks";

const tempBehavior$ = new BehaviorSubject({ hello: "message" });

export default function App() {
  const tempBehavior = useObservableState(tempBehavior$);

  return <div>{tempBehavior.hello}</div>;
}

Test Link is here: https://codesandbox.io/s/aged-butterfly-rqzwb?file=/src/App.tsx

the typing about this form should be like that

export function useObservableState<TState>(
  input$: BehaviorSubject<TState>
): TState

rxjs version: 6, 7 (both not working)

Suggestion: overload `useSubscription` to add support for observer objects

The subscribe function of an Observable can receive an observer object:

const observer = { next: myNext, error: myError, complete: myComplete }

ob$.subscribe(observer)

It would be great if useSubscription allowed the same, so we can do this:

useSubscription(ob$, observer)

… instead of this:

useSubscription(ob$, observer.next, observer.error, observer.complete)

`useObservableEagerState`: incorrect return type

The type definitions say that useObservableEagerState will always return TState but it's possible for it to also return undefined if the observable provided doesn't emit a value synchronously. Reduced test case: https://stackblitz.com/edit/react-ultydo?file=src%2FApp.js

What do you think about updating the return type to be TState | undefined?

It feels wrong to do that because useObservableEagerState is only intended to be used with observables that do emit a value synchronously—but there's no way for us to enforce that at the type level, as far as I know. (Maybe traits will help?)

Feature request: allow 'useObservableState' callback with no parameters

I'm just starting to use this library, and it looks amazing so far! I'm using the useObservableState function to watch responses from an observable. I need to be able to re-trigger the observable, so I've provided an init function. However, since I am just re-triggering the same observable I don't need to supply any parameters - but the useObservableState function update callback forces you to always supply 1 parameter, even if you specify the callback parameter type as undefined.

It's a small thing, but it would be lovely if when you specify the callback parameter type as undefined (and/or null maybe, or undefined in a type union?), that the callback function type would have the parameter as optional.

const [num, refreshNum] = useObservableState<number, undefined>(
  () => getNumExternal$()
);

refreshNum(undefined)   // currently enforced format
refreshNum()            // proposed format to be allowed

Why not replace Redux?

From the README:

This library is not for replacing state management tools like Redux but to reduce the need of dumping everything into global state.

Replacing Redux with this sounds like a great idea to me; why do you recommend against it?

useSubscription typescript wont allow undefined or null input$

In the docs it says that the useSubscription hook will take an input$ (first parameter) of Observable<T> | null | undefined, but the typescript type definition only allows input$ to be of type Observable<T>. Is this intentional (and so a mistake in the docs), and mean the hook requires an observable input$, or is it a mistake in the typing definitions?

RxJs 7.0 support

Is support planned for v7 or any timeline we can expect to see this being supported?

Question: best practice for integrating observable hooks into error boundaries and suspense

Hello,

I'm new to React but have some experience with RxJS. The answers here might be self evident to those with more experience. I'll ask anyway because the intersection of the two doesn't seem to be very common yet.

In this example, different React components are piped depending on the state of the observable:
https://observable-hooks.js.org/examples/#conditional-rendering-vanilla-javascript

There is one JSX block that represents a loading state, another a steady state (with a value) and then there's also an error JSX in case of error. This looks really great if the component is managing each of these states and I find it nice and readable there in an RxJS sense.

What happens if the component is deep in a hierarchy of components and it doesn't handle the loading and/or error boundaries? I'm assuming that it wouldn't have any startsWith() or catchError() handlers in the pipe. But then, how will the parent components get triggered for suspense or error boundaries?

Is there a recommended way of handling this case?

Thank you,
Chris

Suspense, loop of XHR requests and suggested code structure

Hi,
First, great project, thanks!

Second: I quickly ran into the issue more clearly stated in the use-suspense project, I think adding a similar note here would be very helpful.

Beware
Due to the design of Suspense, each time a suspender is thrown, the children of Suspense Component will be destroyed and reloaded. Do not initialize async data and trigger Suspense in the same Component.

So: related to the above design choice of Suspense, do you have a recommendation of how to structure the code? Let's say I have MyComponent containing logic and MyComponentUI containing only the interface parts. The api contains a method returning a $apiStream which generates an XHR when subscribed to.

Would the suggested design be to place the new ObservableResource($apiStream) in MyComponent and then pass that as a prop to MyComponentUI where useObservableSuspense(observableResource) is called?

Would you recommend "caching" the observableResource between different resources as an in-memory solution? If so, when would the XHR be generated?

Lastly: I actually found this project looking for a less hacky solution to trigger an ErrorBoundary in async events (observables typically) than this.setState(() {throw new Error("will be caught by ErrorBoundary")}). Any thoughts since observable-hooks swallows errors currently if I understood the other discussions correctly.

Thanks again for a great project!
/Victor

Auto unsubscribe on the element is detached

Thanks for the library.

Let's say I have the following example:

function App() {
  const [show, setShow] = React.useState(true);
  const [text, updateText] = useObservableState<string>(
    text$ => text$.pipe(delay(1000), map(() => Math.random().toString()), finalize(() => console.log('done'))),
    'Init'
  )

  return (
    <section>
      <button onClick={() => setShow((v) => !v)}>Toggle</button>
        {show && <h1 onClick={updateText}>{text}</h1>}
    </section>
  )
}

When we toggle the element the subscription is still alive. The finalize isn't invoked. How do we deal with this situation?

Thanks.

Btw, I also get a type error:

image

Feature proposal: A hook to create a Observable State

export default function useCreateObservableState<T>(initialState?: T): [Observable<T>, (v: T) => void] {
  const subject = useMemo(() => new BehaviorSubject<T>(initialState), []);
  const observable$ = useMemo(() => subject.asObservable(), [subject])

  const updateValue = (v: T) => {
    subject.next(v);
  };

  return [observable$, updateValue];
}

I don't know if this even makes sense. But maybe creating a BehaviorSubject in a Reactish way is convenient. It'll be useful to provide an observable state store via ContextAPI (as a dependency injection tool).
I think that the usage of Context API + useState/useReducer doesn't scale and it brings performance issues (such as whole tree rerendering even if a component doesn't consume any value from the provided context)
Eg:

export interface UserState {
  name: string;
}

export const UserStoreContext = createContext<Observable<UserState>>(undefined as any);

export const UserStoreProvider = ({ children }) => {
  const [state$, setState] = useCreateObservableState<UserState>({ name: 'Joe Doe' });

  return <MyStoreContext.Provider value={state$}>{children}<MyStoreContext.Provider />
}

export const FancyUserName = () => {
  const user$ = useContext(UserStoreContext);
  const { name } = useObservableEagerState(user$);
  return <div style={{ color: 'red' }}>{name}</div>;
}

Questions about ObservableResource

Hi crimx:
Nice to see you updated for Suspense,I love this update very much!!!!
When I study your code, I can't understand this Comment:

if (this.handler) {
      const { resolve } = this.handler
      this.handler = null
      // Resolve instead of reject so that
      // the resource itself won't throw error
      resolve()
}

Could you tell me please:

  1. What this reject mean?
  2. How do these code avoid resouce itself throw error?

Unexpected error when using `useSubscription` for a `Subject`

Minimal codes to reproduce:

export default function App() {
  const { current: someValue$ } = useRef(new Subject<number>());

  useSubscription(someObservable$, someValue$);

  // ...
}

Error:
截屏2023-08-15 11 28 53

Stackblitz URL: https://stackblitz.com/edit/stackblitz-starters-uzyyi8?devToolsHeight=33&file=src%2FApp.tsx

Suspicious cause in use-subscription-internal.ts:

const nextObserver =
  (argsRef.current[1] as PartialObserver<TInput>)?.next ||
  (argsRef.current[1] as ((value: TInput) => void) | null | undefined)
if (nextObserver) {
  // BUG: When argsRef.current[1].next presents, this way of calling the function
  // causes `this` to be missing which Subject's `next` relies heavily on.
  return nextObserver(value)
}

A possible fix, would love to open a PR for this:

const observer = argsRef.current[1];
if (observer && 'next' in observer && typeof observer.next === 'function') {
  return observer.next(value);
}

if (observer) {
  return observer(value);
}

IE11 and other old browsers (how to make it work)

Hi,

Just a friendly note for others who might face similar issues. For the longest time we were quite convinced observable-hooks doesn't work with IE11 since IE11 failed spectacularly without any error messages in the console whenever observable-hooks was included and used in some way (despite polyfills etc).

Turns out the issue is that the source code syntax is not ES5 compatible, meaning IE11 doesn't support it. Our webpack build excluded all node_modules from transpilation, meaning syntax was left as-is making IE11 fail on for example () => {} syntax (fat arrow functions). The kicker was that in our project this package is the only one needing transpilation, so it really took time to find the issue and the cause of it.

So: Just a friendly reminder to anyone searching for similar issues: This package needs transpilation (using babel js for example) for IE11 and other older browsers and cannot be excluded like most node_modules can.

@crimx : It would have been very helpful for us with a note about this in the documentation; at least it took us a really long time to track down! Either way hopefully someone will be helped by this issue.

Great package either way, greatly simplifies working with rxJS in React!

/Victor

`useObservableCallback`: allow callback with no parameters, or > 1 parameters

In React we can use useCallback to define a callback with 0 parameters, like so:

const callback = useCallback(() => 1, []);
// no error
callback();

If we try to do something similar using useObservableCallback we run into problems:

const [callback, value$] = useObservableCallback((value$: Rx.Observable<never>) => value$);
// error
callback();

The difference between these two hooks is this: useCallback allows the callback to have any number of parameters, whereas useObservableCallback requires 1 parameter—no more and no less.

We could make useObservableCallback behave like useCallback:

declare function useObservableCallback<
    TInput extends Readonly<any[]>,
    TOutput = TInput,
    TParams extends Readonly<any[]> = TInput
>(
    init: (events$: Observable<TInput>) => Observable<TOutput>,
    selector?: (args: TParams) => TInput,
): [(...args: TParams) => void, Observable<TOutput>];

// 1 param
{
    const [callback, value$] = useObservableCallback(
        (value$: Observable<[number]>) => value$,
    );
    // no error
    callback(1);
}

// > 1 params
{
    const [callback, value$] = useObservableCallback(
        (value$: Observable<[number, string]>) => value$,
    );
    // no error
    callback(1, 'foo');
}

// 0 param
{
    const [callback, value$] = useObservableCallback(
        (value$: Observable<[]>) => value$,
    );
    // no error
    callback();
}

This gives us the most flexibility but it would be a breaking change because now the observable receives an array of parameters instead of just one parameter.

useSubscription and useObservableState cause duplicate requests

When either hook is used in coordination with ajax.getJSON() (or similar observable), it acts as if 2 subscriptions are made as there are 2 XHR requests made for each hook.

Minimally reproducible example: https://codesandbox.io/s/quizzical-bassi-xcetk?file=/src/App.js

Monitor the network tab to see that 2 requests are made but only 1 state change is triggered. There are several "examples" in the one sandbox to demonstrate the issue.

When using a normal useEffect(() => stream$.subscribe()), everything works as expected.

Compatible with Expo?

Is there any reason this library wouldn't work with Expo?

I'm using useObservableState a lot in an NPM package I made, and recently made it compatible with Expo. Doing a simple example though, the component doesn't update even though I'm logging the observable I'm looking at and it DOES change in the logs.

Here's the snippet:

export default function Rooms() {
  const rooms = useObservableState(matrix.getRooms$())
  console.log('rooms', rooms)
  return (
    <>
      <Header />
      <View style={{flex: 1}}>
        {rooms?.map(r => (
          <TouchableOpacity style={{ backgroundColor: 'blue', height: 50, width: 100}}>
            <Text>item</Text>
          </TouchableOpacity>
        ))}
      </View>
    </>
  )
}

"rooms" is initially an empty array until it's loaded with rooms shortly after. Those loaded rooms show in the logs, but the mapped list never populates.

如何使用 useObservableEagerState 获取“从无到有”的 BehaviorSubject 值?

示例代码:

const queryService = graphStoreGet.queryService();

const krResult$ = useObservable(
    input$ =>
      queryService.okrSubjectMap$.pipe(
        withLatestFrom(input$),
        filter(([okrSubjectMap, [kdId]]) => Boolean(okrSubjectMap[kdId])),
        switchMap(([okrSubjectMap, [kdId]]) => okrSubjectMap[kdId]),
      ),
    [krItem.item.id],
);
const krResult = useObservableEagerState(krResult$);

okrSubjectMap 初始是一个空对象,类型是 Record<string, BehaviorSubject>,会动态的往里添加值,因此初始调用时会报错: Error: Observable did not synchronously emit a value.

TypeError when using observable-hooks higher up in component tree

Great idea to combine RxJS with React hooks. An issue I have encountered is trying to create an observable higher up in my component tree. For example, consider the following code below:

  const keydown$ = fromEvent(document, 'keydown')
    .pipe(
      throttleTime(1500),
    );
  useSubscription(keydown$, (e: any) => {
    const { key } = e;
    if (key === 'PageUp' || key === 'PageDown') {
      e.preventDefault();
      e.stopPropagation();
      console.log(e);
    }
  });

Lets say my app structure looks something like Root -> AppContainer -> SomeComponent.
Putting the above code in the AppContainer component throws the following TypeError: TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

However, putting the exact same code in SomeComponent works as expected. I believe this issue is related to the observable hook and not RxJS because if I just create the observable and console.log() it without the subscription, I can see it's getting created fine. Any ideas? Thanks!

useSubscription 会触发 JS 报错导致页面白屏

描述

我们项目在遇到 http 报错的时候整个页面会白屏。

原因分析

useSubscriptionInternal 中在调用方未申明 subscribe 的 error handle 的时候,会把当前的 error throw 出来,触发了 JS 报错,导致页面白屏 - 在项目 use-subscription-internal.ts 的 90-93 行,备注了为了项目可以用 error boundary catch。

可以用这个例子查看

为什么修改

在 RXJS 的原生行为中,如果触发了 error 应该在 catchError 这个 operator 或者 subscribe 的 error handle 阶段去处理,本身的订阅不会触发 JS 报错而是 console.error 打印出 error。而 useSubscription 的这种行为会导致我们需要在所有调用 service 的地方加上 catchError,否则一旦接口出现问题,就会导致页面直接白屏。

如何修改

可以考虑加配置来供业务方配置是否需要 throw error,也可以考虑让业务方自行处理 RX 中的 error:即取消这一段 throw error 的代码

如何实现替换react官网的useEventCallback?

下图是react官网对useEventCallback的描述,文档里表示在任何情况下都不建议使用这个函数,
image
我想使用observable-hooks库尝试实现替换react原生不被推荐的useEventCallback

但是看了源码,发现useSubscription内的observer也是使用useRef实现的,那这样还是会存在渲染阶段的问题,我数据获取的时候只会获取到useRef初始化地方的变量,如果渲染阶段,在useRef后变量更新了,或者是在React并发模式下,还是会存在和useEventCallback一样的问题。

我如果将所有依赖都切换成Observable感觉处理起来会很麻烦,请问官方是否有合理的替代方案呢?

下面是我的代码:

export default function usePersistCallback<T>(
  observer?: (value: T) => void,
  init?: (events$: Observable<T>) => Observable<T>,
) {
  const [onEventTrigger, onEventTrigger$] = useObservableCallback(init);

  useSubscription(onEventTrigger$, observer);

  return onEventTrigger;
}

我将传给useEventCallback的函数替换成了usePersistCallback内的observer。

违背hooks原则

hooks是依赖顺序的,以下代码应该会报错,至于为啥违反了react原则又没报错,这个很有水准

export function useObservableInternal<TOutput, TInputs extends Readonly<any[]>>(
  useCustomEffect: typeof useEffect,
  init:
    | (() => Observable<TOutput>)
    | ((inputs$: Observable<[...TInputs]>) => Observable<TOutput>),
  inputs?: [...TInputs]
): Observable<TOutput> {
  if (!inputs) {
    return useRefFn(init as () => Observable<TOutput>).current
  }

  const inputs$Ref = useRefFn(() => new BehaviorSubject(inputs))
  const source$Ref = useRefFn(() => init(inputs$Ref.current))

  const firstEffectRef = useRef(true)
  useCustomEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$Ref.current.next(inputs)
  }, inputs)

  return source$Ref.current
}

Question: init function call multiple times in StrictMode

import * as React from "react";
import * as ReactDOM from "react-dom";
import { of } from "rxjs";
import { map } from "rxjs/operators";
import { useObservable, useObservableState } from "observable-hooks";

const { log } = console;

function App() {
  const a = useObservable(() => {
    log("a");
    return of(1);
  });
  const b = useObservable(() => {
    log("b");
    return a.pipe(map((x) => x + 1));
  });

  const [c] = useObservableState(() => {
    log("c");
    return b;
  });
  return <>{c}</>;
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

run codes in development mode and strict mode

It will print

a
b
c
a
b
c

init function in useObservable and useObservableState are called twice.

Is it concurrent mode safe? Is it work as intended?

Specify custom dependencies for `useObservableSuspense`

I wrote this custom hook that uses the useObervableSuspense to retrieve an Observable<T> value from a custom AnyRepository class (implementation not shown) based on an id string.

export function useFindSuspense(name: RepositoryName, id: string) {
  const repository = useAnyRepository(name);
  const find$ = repository.findById(id);
  const resource = new ObservableResource(find$, value => !!value);

  return useObservableSuspense(resource);
}

The value of the ID is gotten from the react-router (v6) useParams() hook like so:

import {useParams} from 'react-router';

const DetailComponent = () => {
  const deck = useFindSuspense('decks', deckId);
  // render component
};

const Page = () => {
  const {deckId} = useParams()
  // this is wrapped in a Suspense component
  return <DetailComponent deckId={deckId} />
}

and is passed to the custom useFindSuspense hook.

Currently, when the route is changed, the useObservableSuspense hook returns the same value.
This is despite the underlying Observable<T> and find$ changing and also the id/deckId input.

I've tried to the following:

  1. Memoize the Observable Resource
export function useFindSuspense(name: RepositoryName, id: string) {
  const repository = useAnyRepository(name);
  const resource = useMemo(() => {
    const find$ = repository.findById(id);
    const observableResource = new ObservableResource(find$, value => !!value);
    return observableResource;
  }, [id, repository]);

  return useObservableSuspense(resource);
}

Note: Adding this line of code:

useEffect(() => {
    console.log('Changed');
  }, [resource]);

prints the Changed to the console as expected (on route change) but the value returned from the hook and the useObservableSuspense hook is unchanged.

  1. Call useEffect to manually update the resource
export function useFindSuspense(name: RepositoryName, id: string) {
  const repository = useAnyRepository(name);
  const find$ = repository.findById(id);
  const resource = new ObservableResource(find$, value => !!value);
  useEffect(() => {
    resource.reload(repository.findById(id));
  }, [id]);

  return useObservableSuspense(resource);
}

Both of which did not do anything, even when the ObservableResource itself changed and it's inputs have changed.

So I assumed that this was because there's no way to specify custom dependencies for the useObservableSuspense hook, which could be "re-calculated" if those dependencies changed.

I will go and try patch the hook to get a working solution, but I think that is what might be nescessary.

[Question] Why useMemo and useRef instead of useEffect?

I'm sorry for hijacking issues for a question, but I've struggled to find an explanation on my own and I think that this might be useful for other people.

On more than one occasion, Google search produces a common pattern for subscribing to observables in React context:

React.useEffect(() => {
  const sub = observable$.subscribe();
  return () => sub.unsubscribe();
}, [])

In most cases, this works fine, but sometimes, it fails spectacularly. For example, I've encountered a situation in which

React.useEffect(() => {
  const sub = combineLatest(o1$, o2$, o3$, o4$).subscribe();
  return () => sub.unsubscribe();
}, [])

it getting initialized, but somehow combineLatest fails to subscribe to the observables o1$ through o4$ properly, while doing combineLatest(o1$, o2$, o3$, o4$).subscribe() just outside the useEffect hook works as expected. I've tried creating a minimal test case for this situation, but failed, since most of the times, that useEffect pattern does work and I have not been able to figure out which parts of my observables make it fail.

Now, the epiphany: I've tried replacing my useEffects with useSubscription from observable-hooks and sure enough, my problematic code started working again. After I've looked into useSubscriptions code, I've noticed that useEffect is only used for unsubscription, subscription is handled via useMemo and useRef.

Are there any reasons why you implemented useSubscription this way? Is there any explanation why this combination works better that useEffect?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.