re-rxjs / react-rxjs

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

Is a more concise API possible? #318

Closed jbhoot closed 3 weeks ago

jbhoot commented 1 month ago

I use this library to manage state in a project at work. I love it.

Though I wish that the react-rxjs integration interface were simpler and less verbose than it is.

Currently, a minimum of 4 variables need to be tracked for one state stream:

This hurdle becomes too much for newcomers.

Is there a way to simplify this interface?

josepot commented 1 month ago

Is there a way to simplify this interface?

I think so. Could you please provide a concrete example so that we can suggest a less verbose approach? Or perhaps we can come up with a less verbose API.

jbhoot commented 1 month ago

Sure, here is an example. Forgive any syntax error in the code; my original code is in ReScript (a language that transpiles to JavaScript), and I translated it to JavaScript here for convenience.

let [sValueChanged, handleValueChange] = createSignal(e => getEventValue(e));

let [useRawValue, ssRawValue] = bind(sValueChanged, '');

let [useParsedValue, ssParsedValue] = bind(
    ssRawValue.pipe(
      map(v => {
        // return Ok(int) or Error(string).
      }),
    ),
)

const Threshold = () => {
  const rawValue = useRawValue();
  const parsedValue = useParsedValue();
  const id = "ThresholdInput";
  return <div>
    <label htmlFor={id}>Threshold you would like to cover</label>
    <div>
      <input
        type="text"
        id={id}
        name={id}
        defaultValue={rawValue}
        onChange={handleValueChange}
      />
      <span>tonne/hectare</span>
    </div>
    {parsedValue.error && <p>{parsedValue.error}</p>}
  </div>
}
jbhoot commented 1 month ago

Please let me know if you need more input from me.

voliva commented 1 month ago

I personally prefer the newer API that uses useStateObservable hook instead - It's probably due to personal preference, but that decreases the amount of different variables you'd be using. With this you could rerwite it to:

import { state, useStateObservable } from '@react-rxjs/core'
import { createSignal } from '@react-rxjs/utils';

const [sValueChanged, handleValueChange] = createSignal(e => getEventValue(e));

const ssRawValue = state(sValueChanged, '');

const ssParsedValue = ssRawValue.pipeState(
   map(v => {
     // return Ok(int) or Error(string).
   }),
)

const Threshold = () => {
  const rawValue = useStateObservable(ssRawValue);
  const parsedValue = useStateObservable(ssParsedValue);
  const id = "ThresholdInput";
  return <div>
    <label htmlFor={id}>Threshold you would like to cover</label>
    <div>
      <input
        type="text"
        id={id}
        name={id}
        defaultValue={rawValue}
        onChange={handleValueChange}
      />
      <span>tonne/hectare</span>
    </div>
    {parsedValue.error && <p>{parsedValue.error}</p>}
  </div>
}

As for getting it more concise, something that we've seen a common pattern is the createSignal + state combo, but I feel like it's trivial to build abstractions that fill their own needs, so at the moment I'd rather not increase the API surface. But having something like:

export const createStatefulSignal = <T, I>(mapper: (input: I) => T, defaultValue: T) => {
  const [valueChange$, setValue] = createSignal(mapper);
  const state$ = state(valueChange$, defaultValue);

  return [state$, setValue]
}

Might be enough for your use case. But again, it's not something we want to add at this moment because there are different versions which might fit better different use cases (such as also exporting the valueChange$, in regards to the mapper, or defaultValue, or having it reset, etc.), so I think it's better to let everyone deal their own utilities for this, and leave React-RxJS with smaller composable primitives.

With this utility the code would be reduced to:

const [ssRawValue, handleValueChange] = createStatefulSignal(e => getEventValue(e), '');

const ssParsedValue = ssRawValue.pipeState(
   map(v => {
     // return Ok(int) or Error(string).
   }),
)
jbhoot commented 1 month ago

Wow. So, state does the same thing as bind.

I personally prefer the newer API that uses useStateObservable hook instead.

I looked in the docs further, and saw this in the bind API page:

function bind<T>(
  observable: Observable<T>,
  defaultValue?: T,
) {
  const state$ = state(observable, defaultValue);

  return [
    () => useStateObservable(state$),
    state$
  ];
}

So, state is a lower-level abstraction used by bind, even though the former feels simpler to use.

My guess is when you referred to the state APIs as the newer API, you might have been talking about useStateObservable, not the state() function.

jbhoot commented 1 month ago

createSignal + state combo : createStatefulSignal()

I've given this a shot, but as you described, this abstraction faltered for a few scenarios, like producing two state streams that depend on a single valueChange$.

However, now that I'm writing this, I realised that I can just use the shared stream returned by createStatefulSignal to produce two state streams. I'm missing something here.

Anyway, state + useStateObservable is a much more intuitive API than bind + useNameAHandlerForEveryStream API to me. A Jotai user would object less to the former API.