Is a more concise API possible?
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:
- handleFn to dispatch events from react component
- a stream that receives the events dispatched from a React component
- a stream the access to which can be shared across React components
- a
useXhook to access a value from the shared stream
This hurdle becomes too much for newcomers.
Is there a way to simplify this interface?
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.
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>
}
Please let me know if you need more input from me.
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).
}),
)
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.
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.
In case this helps anyone, I came up with an abstraction tailored to my needs.
The original code is in ReScript, which supports pattern matching that makes the abstraction simpler. But a roughly translated TypeScript is as follows:
import { startWith } from "rxjs/operators";
import { state } from "@react-rxjs/core";
type FirstEmission<v> =
| { t: 'SuspendUntilStreamEmits' }
| { t: 'EmitAsFirstValue', v: v }
type WhoManagesSubscription<v> =
| { t: 'ConsumerComponent', preSubscriptionValueForHook: v }
| { t: 'SubscribeTreeRoot', firstEmission: FirstEmission<v> }
const stateX = (stream, whoManagesSubscription) => {
switch (whoManagesSubscription.t) {
case 'ConsumerComponent':
return state(stream, whoManagesSubscription.preSubscriptionValueForHook)
case 'SubscribeTreeRoot':
switch (whoManagesSubscription.firstEmission) {
case 'SuspendUntilStreamEmits':
return state(stream);
case 'EmitAsFirstValue':
return state(stream.pipe(startWith(whoManagesSubscription.firstEmission.v)));
}
}
}
// usage
let sHandledByConsumerComponent = stateX(sNameChanged, { t: 'ConsumerComponent', preSubscriptionValueForHook: '' })
let sHandledBySubscribeRoot = stateX(sNameChanged, { t: 'SubscribeTreeRoot', firstEmission: { t: 'SuspendUntilStreamEmits' } })
let sHandledBySubscribeRootStartWith = stateX(sNameChanged, { t: 'SubscribeTreeRoot', firstEmission: { t: 'EmitAsFirstValue', v: '' } })