cassiozen / useStateMachine

The <1 kb state machine hook for React
MIT License
2.38k stars 47 forks source link

Suggestion: Less awkward way to pass context and event types #53

Closed devanshj closed 3 years ago

devanshj commented 3 years ago

useStateMachine is a curried function (Yummm tasty!) because TypeScript doesn't yet support partial generics type inference. This workaround allows TypeScript developers to provide a custom type for the context while still having TypeScript infer all the types used in the configuration (Like the state & transitions names, etc...).

We no longer have this limitation with #36. We can have something like the following for context...

useStateMachine({
  initial: "idle",
  context: { foo: 1 } // I prefer "initialContext" but whatever xD
  ...
})

And something like this (what xstate v5 does), for events and context...

useStateMachine({
  schema: {
    context: createSchema<{ foo: number, bar?: number }>(),
    event: createSchema<
      | { type: "X", foo: number }  
      | { type: "Y", bar: number }
    >(),
    // I prefer "event" instead of "events" because the type is for event and not "events"
    // (unions need to be named singular even though they seem plural)
    // though "events" works too if that's more friendly
  },
  initial: "idle",
  context: { foo: 1 }
  ...
})
devanshj commented 3 years ago

@cassiozen Waiting for your thoughts on this before I continue working on the PR because if you like this idea and we'd be going with this then will have to redo all the work :P

cassiozen commented 3 years ago

Oh, that looks pretty good. Let's go with it.

devanshj commented 3 years ago

This schema was taken as is it from xstate, I personally don't like it for many reason stated here, it probably will change in xstate too (I'm speaking with David, let's see). Here's a hypothetical narrative to explain the gist.

A: I don't like the fact that if you have 10 events and only one has a payload you have to write all of them instead of defining the payload just for one A: Let's make it...

events: {
  ON_CHANGE: t<{ value: string }>()
}

A: ...so that'll mark ON_CHANGE has a payload of { value: string } and rest events (eg ON_BLUR) that will be inferred from on will have no payloads

B: Hmm... But what if someone makes a typo and the event with a typo gets inferred. For example it user makes a typo ON_BLURR instead of ON_BLUR it won't get caught even if I define it in the schema as events: { ON_BLUR: {} } because we're merging schema and inferred.

A: Okay then let's have...

events: {
  types: ["ON_CHANGE", "ON_BLUR"],
  // in case of `types: undefined` they are inferred
  payloads: {
    // user can have `payloads` even if they didn't define `types` because they will be inferred
    ON_CHANGE: t<{ value: string }>()
    // the events missing are assumed to have no payload
    // meaning writing `ON_BLUR: {}` would have the same effect
   }
}

A: ...in this way people who don't care about typos can simply skip writing types and those who care can write them all. B: Hmm looks good

So I'm proposing we make it schema: { events: { types, payloads } } and if you think no one cares about typos we can make it schema: { events } (the first snippet) too :P Feel free to ask questions if I didn't explain myself clearly!

Edit - I have a better version see "edit 2" section of this comment. Copying it here for reference.

Okay so I figured out a way to make the api simpler at the same time solving the problems. Here's a narrative explanation of how the current api came into being, and here's a simpler version

guards: {
  $$exhaustive: true
  isEnterKey: t<(_: never, event: { keyCode: number }) => boolean>(),
  isShiftKey: t.constraint<(_: never, event: { keyCode: number }) => boolean>()
  isFoo: t.inferred()
}

Now instead of writing all identifiers they can just set $$exhaustive: true (we can even export a symbol if we don't want to reserve "$$exhaustive" as an identifier). Alternatively if we want the default behavior to be exhaustive users can set $$exhaustive: false instead when wanting other identifiers to be inferred.

So in our case the final schema would look something like...

context: t<{ foo?: number }>(),
events: {
  $$exhaustive: true,
  ON_CHANGE: t<{ value: string }>(),
  ON_BLUR: t<{}>()
}

And if the user doesn't care about typos then...

context: t<{ foo?: number }>(),
events: {
  ON_CHANGE: t<{ value: string }>()
}
cassiozen commented 3 years ago

I really like this!

devanshj commented 3 years ago

Cool so I'll update the PR soon and it'll be good to merge!