re-rxjs / react-rxjs

React bindings for RxJS
https://react-rxjs.org
MIT License
550 stars 19 forks source link

Binding to observables that emit SUSPENSE with a default value does not trigger React.Suspense to render the `fallback` component #307

Open amydevs opened 1 year ago

amydevs commented 1 year ago

Overview

I have a component that is using a returned hook from the bind function. Within the docs examples, they trigger a suspense component's fallback by starting with emitting the SUSPENSE symbol on the observable passed to the bind function. However, this does not work when I've passed a default value to bind.

The intention is to have a component that can have initial stale data that is to be hydrated from an observable

Recreation

I've first declared a signal and an asynchronous function:

   const [apiCall$, onApiCall] = reactRxjsUtils.createSignal();

    let i = 0;
    const f = (): Promise<number> => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(i++);
        }, 1000);
      });
    }

Here, I've followed the SUSPENSE page in the API docs in creating a hook, passing in an observable that should start with a value of suspense:

    const [useApiResult, apiResult$] = reactRxjsCore.bind(
      apiCall$.pipe(
        rxjsOperators.switchMap(
          () => concat(of(reactRxjsCore.SUSPENSE), from(f()))
        )
      ),
      null
    );

I've also tried this variation:

    const [useApiResult, apiResult$] = reactRxjsCore.bind(
      apiCall$.pipe(
        rxjsOperators.switchMap(
          () => from(f()).pipe(rxjsOperators.startWith(reactRxjsCore.SUSPENSE))
        )
      ),
      null
    );

And just only emitting SUSPENSE:

    const [useApiResult, apiResult$] = reactRxjsCore.bind(
      apiCall$.pipe(
        rxjsOperators.switchMap(
          () => of(reactRxjsCore.SUSPENSE)
        )
      ),
      null
    );

No matter whatever I do, except for removing the default null argument to bind, the fallback ("Loading...") is not shown when SUSPENSE is being emitted.

function Button() {
  const { onApiCall, useApiResult } = useService("test");
  const apiResult = useApiResult();
  React.useEffect(() => {
    if (apiResult == null) return;
    console.log(apiResult);
  }, [apiResult]);
  return (
    <button onClick={onApiCall}>Click me</button>
  );
}

function Test() {
  return 
  (
    <Subscribe fallback={"Loading..."}>
      <Button />
    </Subscribe>
  );
}
CMCDragonkai commented 1 year ago

This is useful because maybe you want to have a component that still works on the server side rendering, so you need a default value. On top of that, you need to be able to emit SUSPENSE on the observable so you can switch to loading fallback, or emit an error to trigger a error boundary when there's an error.

Basically it's a way of using an observable to reify the 4 states of an asynchronous operation state machine:

  1. Initial default state
  2. Loading state
  3. Success state
  4. Failure with error states

It seems that using a subject that comes out of createSignal, and composing it with a promise and binding it back into a component would be a useful way of doing this.