-
-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
@cycle/state Reducer to expect defined state? #948
Comments
I am actually currently working on the new |
Awesome. Keep up the good work. The idea I have is to separate initialization of state from reducing of state. That way a reducer can assume initialized state. It seems that the only place where initialization of state could occur in that case is at the call to So perhaps withState(main, initial, name) This raises the question whether it is appropriate for the state of a component in this paradigm to be initialized by its parent rather than by itself. I would say that it is. |
These reducers are defined by your app and composed in streams, so that you know the order of emissions of those reducers in the stream |
The problem is more about the types I think, how can we make the stateSource not return a possibly undefined state |
Hmm, but does that happen? I don't recall having to do |
That all pertains to runtime. This issue is regarding the TypeScript types.
I'm not sure what the challenge in that is. If an initialized state is required in
Perhaps |
That's not going to happen for a tiny issue such as this one. Initial state is application logic, so it belongs inside |
I use this signature for state reducer:
|
I suggest a new constructor const withIntiailState = (main, initialState: T, name) => {
const sandwiched = (sources) => {
const mainSinks = main(sources)
return {
...mainSinks,
[name]: (mainSinks[name] as Stream<Reducer<T>>).startWith(_ => initailState)
}
}
return withState(sandwiched, name)
}
|
Sure. If the logic is such that the initial The problem is in the sink type. The reducer demanded by the TypeScript type of the sink is the one that expects a possibly undefined state parameter. When a sink of type When That is why I claim that there's a type problem here. And the type problem exposes a missed opportunity of using type checking to make sure that reducers are always called with initialized state. Currently, to conform with the sink types (with
Initial state does feel like application logic, but also the mere having of state feels like application logic. And the mere having of state is removed outside the component to be the concern of The concept that reduction and initialization of state is application logic makes sense to me. In most programs, state is initialized along with the possible type declaration of it, as soon as it exists. In current @cycle/state, state is practically initialized with the value If it is so important to refrain from such a breaking change as the one I suggested, here is a proposal: export function withState<
So extends OSo<T, N>,
Si extends OSi<T, N>,
T = any,
N extends string = 'state'
>(main: MainFn<So, Si>, name?: N): MainWithState<So, Si, T, N>
export function withState<
So extends OSo<T, N>,
Si extends OSi<T, N>,
T = any,
N extends string = 'state'
>(main: MainFn<So, Si>, initializer?: () => T, name?: N): MainWithState<So, Si, T, N>
export function withState<
So extends OSo<T, N>,
Si extends OSi<T, N>,
T = any,
N extends string = 'state'
>(main: MainFn<So, Si>, b?: (() => T) | N, c?: N): MainWithState<So, Si, T, N> {
const [initializer, name] = typeof b === 'function'
? [b, c ?? 'state' as N]
: [null, b ?? 'state' as N]
// ...
} The down-side of this proposal is that the initial state must be provided as a function that returns the initial state. Another proposal is just a new function that provides a better alternative to withInitializedState(main, initialState, name)
// alternative names
withInitialState(main, initialState, name)
stateful(main, initialState, name) |
This is a TypeScript problem, and it shouldn't leak out to JavaScript users of Cycle.js. Viable solutions are:
Inviable solutions are:
|
@mightyiam did you try how something like |
This would be ideal, but still a breaking change. Current implementation allows the usecase of passing a state initializer
|
Setting an initial state is an app logic realm, should be done inside Main. |
There is an opportunity to provide more type safety to the users. "Don't use
No one wants to do that, right?
The only new strictly-type change that would fix this issue would be the one listed below, adding an additional type parameter to the stream library.
I'm not sure what additional feature in TypeScript you imagine.
Understood to be non-realistic.
It should be considered a serious matter, unless Cycle.js wants to tell its users "don't use Here's another proposal: withState(main, name) // current call, where name is optional
withState(main, { initial: initialState, name }) // new overload This could be a non-breaking change, right? The sink type would depend on whether |
@mightyiam Please quit your insistence on saying this is a serious matter and that the only solution is a new The viable versus inviable solutions I listed are my same arguments. If you use If you proceed to insist on your same arguments, I'll ignore you because I have to spend my time wisely. |
Main having state could also be regarded as app logic. Consider... const main = withState(
(sources) => {
const state$ = sources.state.stream
// ... do your thing ...
const stateReducer$: Stream<(s: State | undefined) => State | undefined> = ...
return {
state: stateReducer$
}
}
) Then... const main = withInitialState(
(sources) => {
const state$ = sources.state.stream
// ... do your thing ...
const stateReducer$: Stream<(s: State) => State> = ...
return {
state: stateReducer$
}
},
initialState
) |
I don't understand where this having argument came from. The only way |
There's probably also a misunderstanding. We typically say that |
@staltz Thanks for explaining. Let me get my head around the Cycle.js architecture. |
I have found that it doesn't make sense to provide initial state in the call to The type of the sink is still an inconvenience, but now I can't imagine a reasonable change in the API. |
How about this: if a stream is provided as the sink, then it reduces a possibly undefined state. If an object with initial state and a reducer stream, then the reducer stream will reduce from defined state. The type of the state sink would be something like this: // Current API
type A<T> = Observable<(s: T | undefined) => T>
// Additional API
type B<T> = {
initial: T
reducer$: Observable<(s: T) => T>
}
type StateSink<T> = A<T> | B<T> This could be backward compatible and provide a means to initialize state on component initialization (which makes sense to me) and prevents ever having Initial state occurs in the component and not outside of it. Also, it guarantees that a component has an initial state emit from its state source stream, which could be considered a benefit, because it could help prevent the no-UI-because-one-of-the-components-did-not-emit-on-DOM situation. |
This just probably need to be set to |
Sinks in Cycle.js are only streams. |
Is that open for discussion? |
As a solution to this corner case TypeScript bug? I don't think so. Small and specific type-level bugs should not require runtime-level shoehorn solutions that make exceptions to the app architecture. |
Every usage of The possibility of this issue being solved somehow in TypeScript itself or in the types of the various stream/observable libraries is practically zero, right? If so, and if this type safety issue is not solved by making a runtime adjustment, then it's pretty much given up on, isn't it? |
I refer again to #948 (comment)
Don't enable |
cyclejs/state/src/types.ts
Line 2 in 313762d
Is this exported
Reducer
meant to be imported and used in apps? I find that in my app I would like to have a reducer type where state is always defined:Could we have a separate API for instantiating state, somehow, so that a
Reducer
could assume positive existence of state?The text was updated successfully, but these errors were encountered: