Git Product home page Git Product logo

usestatemachine's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

usestatemachine's Issues

Todo: add a section on XState

I’m a huge XState fan and this library obviously took a lot of inspiration from it. XState is still the golden standard for state machines in JavaScript, but this library has a more niche use case:

  • It's React specific: it follow react idiomatic patterns (like naming the transition "effect" and following the same useEffect cleanup pattern). Being React-specific also means it can use React’s built-in hooks, useReducer and useEffect do most of the heavy lifting, and as a good consequence this lib is small.

  • It contains a subset of features from XState: I only added to this library functionality that I used over an over again in state machines. I plan to keep it this way (maybe adding simple aktors, will see).

  • It is a little less strict: You can call send from effects, effects can be inlined in the state machine config. I think this makes it more "practical" for daily use at the cost of strict correctness.

Finally, it has a heavy focus on type inference - it's nice when a library can provide autocompletion and type checking without requiring you to manually provide typing definitions.

`send` should be stable across re-renders

If the user is passing send down to child components, and these child components use React.memo or PureComponent, the send method will force a render anyways.

Not a huge issue, but it should be dealt with.

Add TSDoc

TSDoc can add documentation right inside the ide.

Documentation discussion

@cassiozen How and where would you want me to write that document covering typescript features? Like maybe in a "Features" section? Or like in a changelog? (that would be lame haha)

And if I'm going to use shiki-twoslash we'd have to host it somewhere. So should we make a documentation website? Or if all this is on low priority I can simply add a wiki page mentioning typescript features with screenshots?

I'm not sure how to go about this haha

Question about useConstant

function useConstant<T>(init: () => T): T {
const ref = useRef<T | null>(null);
if (ref.current === null) {
ref.current = init();
}
return ref.current;
}

const reducer = useConstant(() => getReducer<Context, Events, State, EventString>(config));
const [machine, dispatch] = useReducer(reducer, initialState);

How is useConstant here better/different from react's useMemo? It looks like it should work same as

const reducer = useMemo(() => getReducer<Context, Events, State, EventString>(config), []);

useMachine return undefined in production environment

I used useStateMachine in a Nextjs project. It is working fine in the development environment, and it run fine when I locally run Next's build files generated after running build command. However, when I deploy it on a server, the server complains nothing is returned by useStateMachine. Can you suggest any way to resolve this?

image

Prevent memory leaks

Consider this example -

const Something = () => {
  let machine = useStateMachine({
    initial: "subscribed",
    states: {
      subscribed: {
        effect: () => {
          let unsubscribe = spawnSomeService();
          return () => unsubscribe()
        }
      }
    }
  })
}

Does anything look fishy here? No right? Except it has a memory leak. The effect cleanup never runs, even after Something is unmounted. So if it's remounted 10 times through out the lifetime of the page, then 10 services would be running instead of 1.

To prevent the memory leak one has to do something like this -

const Something = () => {
  let machine = useStateMachine({
    initial: "subscribed",
    states: {
      subscribed: {
        effect: () => {
          let unsubscribe = spawnSomeService();
          return () => unsubscribe()
        },
        on: { UNSUBSCRIBE: "unsubscribed" }
      },
      unsubscribed: {}
    }
  })
  
  useEffect(() => {
    return () => machine.send("UNSUBSCRIBE")
  }, [])
}

This is not intuitive at all. Not to mention the fact that the effects don't cleanup on unmount is very misleading. So people probably will be having memory leaks in their apps unknowingly.

You see other libraries don't call it "effect" they call them "entry" and "exit". Latter does not have to guarantee that each node that has been entered will always be exited but former has to guarantee that every effect's cleanup will run because the terminology and effect-looking api it chose (which is a good choice as it makes things like this more apparent and cleanups easy to write because the unsubscribers will be in the closure)

So to solve this I propose we have a $$stop event (and rename $$initial to $$start) which would be sent when the component gets unmounted and will basically exit all nodes upto the root (so it'll only show up in cleanup's event parameter and not effect's).

And having a stop event makes sense if you think of spawning the machine one big effect, if it has a start event and does some effects, it should also have a stop event that disposes stuff spawned by the effects.

Trivia:
I was writing the hierarchical implementation where I realized root is the only node which would never exit no matter what, and thought maybe that's why we don't have effect on root as it won't cleanup. But then I realized that's true for all nodes that are active before unmounting.
It's funny I didn't plan to run the root effect, the recursive abstraction demanded me to do it. That's what I like about fp that it demands some consistency, some symmetry, some pattern and if there's something fishy and you don't have these things, then you'd catch it right off the bat as you'd have to go out of your way to do something asymmetric.

Batch `update.send`

Calls to update and send may happen outside of a user interaction, React won’t batch the state updates.

Event type as `never` if any state doesn't declare `on`

on should be optional (final states, for one, won't have any transitions), but currently, if we create a config with any state missing the on key, the Event type turns to never.

This can be seen in the data fetching example: Try invoking send outside of the machine and you will get never as type.

Nested/hierarchical States

Nested states are beneficial for a myriad of reasons, including:

  • State nesting lets you define a new state rapidly in terms of an old one by reusing the behavior from the parent state
  • State nesting lets you get new behavior almost for free, reusing most of what is common from the parent states

Configuration

This is what a nested state machine configuration would look like:

// Traffic Lights
useStateMachine()({
  initial: "green",
  states: {
    green: {
      on: {
        TIMER: "yellow",
      },
    },
    yellow: {
      on: {
        TIMER: "red",
      },
    },
    red: {
      on: {
        TIMER: "green",
      },
      // Nested state: Pedestrian traffic light
      initial: "walk",
      states: {
        walk: {
          on: {
            PED_COUNTDOWN: "wait",
          },
        },
        wait: {
          on: {
            PED_COUNTDOWN: "stop",
          },
        },
        stop: {},
      },
    },
  },
});

State value representation:

The returned machine state would still have a string representation for the state value. Nested stated will be represented with dot syntax. For the above example, possible states are:

  • green
  • yellow
  • red.walk
  • red.wait
  • red.stop

Transitions & Effects

For every transition, the state machine should traverse the config three (depth-first) and find the shortest path for the transition.
Effects should be invoked on parent state only once: transitioning between child states should not re-fire parent effects.

Debug mode

Add a debug mode, where all operations are logged:

  • Transition successful
  • Transition denied
  • Context changed

Should use process.env.NODE_ENV === 'dev' to let bundlers strip the messages.

Top level events

Right now events (under the "on" key) can only be declared within a state. top-level events can be helpful to avoid redundancy / keep the state machine terse.

Parameters when transitioning states

It looks like there is currently no way to provide external information when transitioning states, I read the source code for send, and the examples (such as fetch) seem to internalise the external effect so that the data is available within effect. Apologies if I did a poor job of looking.

My use case is moving data from a subscription callback into the state machine. I need to manage an external lifecycle (add/remove subscriber), and data is supplied to me via callback to the subscriber. Firebase's realtime database is a concrete example:

const [state, send] = useStateMachine({firebaseValue: })({
  initial: 'loading',
  states: {
    loading: {
      on: {
        SUCCESS: 'loaded',
        FAILURE: 'error',
      },
      effect(_, update, /* ??? */) {
        update(context => /* External value would be placed into the context here. */)
      },
    },
    // …
  },
})

React.useEffect(() => {
  // A hypothetical `send` API with parameters.
  const success = value => send('SUCCESS', value)
  const failure = error => send('FAILURE', error)
  const ref = firebase.database().ref('some/path')
  // Manage the subscriber lifecycle.
  ref.on('value', success, failure)
  return () => {
    ref.off('value', success)
  }
)

I realise it would be possible to achieve this with a ref, but I would prefer not having one foot in the door in terms of state and context.

Types Roadmap

  • Provide a hack-free safe degraded version of types. Because the currect depend on some likely typescript non-guarantees that might break, So if some users would like to trade safety over precise types and developer experience they can use that version. And we can also be less worried of making workarounds because we have already warned "Hey this might break" :P
    Two ways of going about this -

    • @cassionzen/usestatemachine/safe (temporary name)
    • declare module "@cassionzen/usestatemachine" {
        interface TypeOptions {
          safe: true
        }
      }
      Alternative names to "safe" - workaroundFree, hackFree,
      Current workarounds - InferNarrowestObject which is almost same as this and Definition.FromTypeParameter (TODO: document what's happening here)

    For milestone v1.0.0

  • Support guard's that are actually typed as type guards. Meaning this should work -

    const eventHasFoo = (state: { event: { foo?: string | undefined  } }): state is { event: { foo: string } } =>
      typeof state.event.foo !== "undefined"
    // I always wanted to publish a library provides authoring predicates that infer guards, which I might :P
    // Meaning with that library the above can be replaced by something like
    // let eventHasFoo = P.doesHave("event.foo", P.isNotUndefined)
    // and if they write it in the machine itself (which they should) they'd also get completions and errors via contextual inferrence.
    
    useStateMachine({
      schema: { events: { X: t<{ foo?: string | undefined }> } },
      initial: "a",
      states: {
        a: { on: { X: { target: "b", gaurd: eventHasFoo } },
        b: {
          effect: ({ event }) => {
            let x: string = event.foo
            // without `gaurd: eventHasFoo` foo would have been `string | undefined`
          }
        }
      }
    })

    It's great the signature is ({ context, event }) => ... instead of (context, event) => ... like xstate, because latter is principally incorrect and consequently has some cons

    For milestone v1.1.0

  • Provide (probably opt-in) linting

    • noInitialToDeadStateNodes
      // needs to be written once per (tsconfig) project
      declare module "@cassionzen/usestatemachine" {
        interface TypeOptions {
          noInitialToDeadStateNodes: true
        }
      }
      
      useStateMachine({
        initial: "b", // Error(noInitialToDeadStateNodes): "b" is a dead state node
        states: {
          a: { on: { X: "b" } },
          b: {}
        }
      })
    • noNonExhaustiveEventsSchema -
      useStateMachine({
        schema: {
          events: { // Error(noNonExhaustiveEventsSchema): `$$exhaustive` not set to `true`
            X: t<{ foo: string }>()
          }
        }
      })
    • noSchemalessDefintion
      useStateMachine({ // Error(noSchemalessDefintion): `schema` is not set
        initial: "a",
        states: { a: {} }
      })
    • more? I don't use state machines so I'm not sure what else we could come up with haha

    For milestone v1.2.0

  • more?

Send should accept a payload

Use case: forms or subscriptions
The user wants to update the context with data coming from outside, like a form or a subscription.
We don't want to expose the updater function to the outside world, so having send accept a payload and making it available to the updater function would be a solution.

matches function on state prototype

Right now the state value is always a string, but since we plan to introduce hierarchical fsms in the future (#21), state values might be represented differently.

Adding a 'matches' function on the machine state prototype (like XState) will give users a consistent and easy way to derive ui from the machine state.

"$$initial" event discussion.

Hey @devanshj, in this example, TypeScript is erroring because of the "$$initial" event:

const [machine, send] = useStateMachine({
  schema: {
    context: t<{time: number}>(),
    events: {
      STOP: t<{resetTime: number}>()
    }
  },
  context: {time: 0},
  initial: 'idle',
  states: {
    idle: {
      on: {
        START: 'running',
      },
      effect({setContext, event}) {
        setContext(() => ({ time: event?.resetTime }));
//                                       ^- Property 'resetTime' does not exist on type '{ type: "$$initial"; }'
      },
    },
    running: {
      on: {
        STOP: 'idle',
      }
    }
  },
});

send({ type: 'STOP', resetTime: 10 });

I think it might be confusing for the user to understand what's going on here. And since so far we're not really using the initial event to trigger anything, I'm thinking of reverting back to start undefined. Any objections?

Add @x-state/fsm XState comparison in the wiki

Hi,

First great work! Can't wait to start use this on my projects from now on.

I just want to suggest, if you find suitable, that in the wiki page in comparison with XState, that if for some reason people are looking for a minimal implementation of XState for another use-cases that are not React (lambda is one I could think on the top of mind), they can use @xstate/fsm.

Thanks and again congrats on the great work!

Shorthand syntax for updating context

Right now, the context updater function behaves almost like a reducer: it gives you the current context and you have to immutably return the next context.

It would be nice if it merging syntax like the old React's component-based setState.

RFC: New effect & update Signature

Request for comments for new update signature inside effect.

@RunDevelopment, @dani-mp, @RichieAHB, @icyJoseph, and anyone interested.

Problem Statement

One issue we currently have within effects is that even though send and update are different functions, they're frequently used together (update the context, then trigger a transition). E.g.:

effect(send, update) {
  const fetchData = async () => {
    let response: Response;
    try {
      data = await someApi();
      update(context => ({ ...context, data: data }));
      send('SUCCESS');
    } catch (error) {
      update(context => ({ ...context, error: error.message }));
      send('FAILURE');
    }
  };
  fetchData();
}

Proposal (Updated)

The proposal is to pass a new assign parameter to effect. It's a function that takes an Object as argument with the type:

{
    update?: (context: Context) => Context;
    event?: EventString | EventObject<EventString>;
}

The above fetch example would look like this:

effect(assign) {
  const fetchData = async () => {
    let response: Response;
    try {
      data = await someApi();
      assign({update: context => ({ ...context, data: data }), event: 'SUCCESS'})
    } catch (error) {
      assign({update: context => ({ ...context, error: error.message }), event: 'FAILURE'})
    }
  };
  fetchData();
}

Proposal (Old)

The proposal is to keep passing both send and update (as well as event, in the future) to effect, BUT with one change: update will also return send, so you can couple updating the context and sending event on a single line:

update(updaterFn)('SUCCESS');

Granted, it might look a little weird at first, but it has a few advantages:

  • You can still just send or just update (keep a simple API and backward compatible)
  • It relates to how the state machine is configured (curried function, context first, state machine second) - can aid learning/remembering the API because it's mnemonic.

Alternative Considered

We could remove the update method from effect and overload send to also update the context:

send(event: string)
send(event: string, updater: fn)
send(updater:fn)

I see a few disadvantages here:

  • Send would have many different overloaded signatures, which could be confusing.
  • Since we don't want to allow free mutations of the context outside an effect, the send method inside effect would be different from the send returned when calling useStateMachine. We might need to come up with different nomenclature or incur the risk of making the API confusing.

Thoughts? Ideas? Comments?

Suggestion: mayTransition function to check if a `send()` would pass guards.

What

Return a function state.mayTransition that allows a user to ask "if I sent this event, would it pass the guards on transitions?"

Usage:

const [state, send] = useStateMachine()({
  initial: "inactive",
  states: {
    inactive: {
      on: {
        ENABLE: "active",
      },
    },
    active: {
      on: {
        DISABLE: {
          target: "inactive",
          guard: () => false,
        },
      },
    },
  },
});

state.mayTransition("ENABLE"); // true
send("ENABLE");
state.mayTransition("DISABLE"); // false

Why

Allow selectively rendering or disabling UI input - such as navigation or submission buttons - based on whether the intended event would pass all guards for the transition. (This would be lazily executed, in case someone's machine has side effects in guard().)

From quick testing, this would be a very small code increase.

xstate?

As far as I can tell, xstate doesn't support this, but I don't know why - or I don't know how to search for it. It's normal for Ruby state machines, though.

Name

mayTransition might not be a great name! I'm not sure what makes sense... maySendEvent? wouldEventBeAccepted? Naming is hard.

Merge send & update in effects

Effects provided two functions as parameters: send & update.
In many cases (as illustrated by the async example), send & update will be used together.
Maybe instead of two separate functions, we should have an overloaded 'send':

send(event: string)
send(event: string, updater: fn)
send(updater:fn)

Branding?

I was wondering if you want to give some name to your library other than "useStateMachine". Mainly because saying "useStateMachine" doesn't ring bells that I'm talking about your library, it could be a generic hook or even other libraries out there. So right now I mention it like "Cassio's useStateMachine".

It doesn't have to be fancy something, "CState" (like XState but C for Cassio) also works haha, just like how "M" in "Mobx" is for Michel. Or if you have something original out of the world in mind that would be even better!

In case your mother tongue is not English, you can name the library whatever "state" is called in your mother tongue. That's what I do sometimes xD

[Question] How to update context without changing state?

Often pieces of state will be needed for decisions in the effect methods. Would you recommend keeping these instead in an external useState, or is there some way to pass these in via events (without changing state).

for instance, in xstate you can do something like this:

{
   states: {
      start: {
         on: {
            readCountChange: {
               actions: [assign((context, event)=>{ return { readsExist: event.readCount > 0}})]
            }
         }
      ...
    }
}

And then use that readsExist later in a condition to decide if some other event should be allowed to make a state change.

Experiment: Extract context mutations to type level.

Problem:
Let's say you're working with a state machine with nodes a and b and context as A | B, now you know that in node a the context is always of type A and in node b the context is of type B. But the context receive in the effect will be of type A | B for both nodes and the user would have to make assertions.

xstate's solution:
The way xstate solves this is by letting the user define the relation between state and context via the "TTypestate" type parameter and "typestate" being an actual thing.

txstate's solution:
xstate has an advantage that the context is immutable, the only way to mutate it is via assign. So all mutations can be found by looking at the type of the machine and hence the typestates can be derived and the user need not write them manually. Explained in detail here

useStateMachine's solution:
Thing is right now there isn't a way to solve this because the context is fully mutable, not only it is mutable but it can be mutated many times inside an effect (you can spin up a setInterval which changes the shape of context each time)

I've opened this issue to sort of discuss if we can improve on this. My first attempt is to make effect an generator, then the user can basically emit mutations and the they will be extracted to the type as typescript can infer generators too (hover over effect) and it seems there is a lot of prior art in using generators as effects (redux-saga, fx-ts, et al). But the problem is right now effect is even capable of "reducing" the context instead of just setting it. Which basically comes in the way.

So my first question is if setContext does not take a reducer do you think there will be things user won't be able to do? Like the effect only receives a context which would be on the entry then there's no way to determine the current context, you think that'd make a big difference?

Also more important question haha - you think the problem that I point out is big enough that we even think about solving it? If not then we can close this (to mark the stance) and still keep discussing if both of us are still interested.

[QUESTION] Conditional transitions and guards

Hey! 👋🏽

I've been exploring the library and I was wondering how guards fit when talking about "conditional transitions" - I'm not sure that's the proper name for the scenario below.

Consider the following scenario:

  1. Initial state has an effect that performs some action - doesn't matter if sync or async
  2. Based on a condition, it should transition to one of two possible states - A or B

My question is about what one should do in other to transition to A or B.

Currently, the guard feature works when transitioning to a specific state - it can either proceed with the transition to that state or stay in the current state, depending on the boolean value returned by the guard function.

From what I understood, the only way to perform a "conditional transition" is doing the check inside the effect and calling send accordingly.

Example
initial: {
  on: {
    GO_TO_A: 'A',
    GO_TO_B: 'B',
  },
  effect(send) {
    send(condition ? 'GO_TO_A' : 'GO_TO_B');
  },
},

I was wondering if it fits the state machine model (and this library's goal of course) to have guarded transitions where one could transition to the next state based on the guard result. Something like, "if guard returns true, go to A, otherwise go to B".

Example
// the names are just for clarification purpose
initial: {
  on: {
    NEXT_STATE: {
      guard: (...args) => boolean,
      targetIfTruthy: 'A',
      targetIfFalsy: 'B',
    },
  },
  effect(send) {
    send('NEXT_STATE');
  },
},

xstate has guarded transitions, but the signature is an array and I think one can put as many {target, cond} items as needed.

My question/proposal relates to conditionally transitioning between two states but I linked their approach in case you see the value and decide to implement the feature similarly - with many possible next states.

If something above isn't clear or if there's a way to accomplish what's proposed here, let me know.

Make "creating" and "using" a machine seperate

The fact that useMachine “creates” the machine means that we can only create a machine inside an component. This problematic because...

  1. You can't define a mahine outside the component (eg)
  2. You can't get the type of the (created) machine outside the component (which is required for context, eg)

createDefinition solves 1 but doesn't solve 2 so we need something like xstate ie let m = createMachine(d); then later in component useMachine(m). We could add an overload to do useMachine(d) which saves users from writing useMachine(createMachine(d))

Access context inside effect

Hey! 👋🏽

I'm trying out this library and I'm wondering about accessing the context within an effect.

My question regards something like this:

  1. Effect on initial state updates context and transits to another state
  2. Effect on the next state uses data set by the previous effect <- this is the part I'm asking about
Example
import useStateMachine from '@cassiozen/usestatemachine';

const [state, send] = useStateMachine({ count: 0 })({
  initial: 'first',
  states: {
    first: {
      on: { second: 'second' },
      effect(send, update) {
        update(context => ({ count: context.count + 1 }));
        send('second');
      },
    },
    second: {
      effect(send, update) { // <- context isn't accessible here
        console.log(state.context.count); // is this the only/correct way to access it?
      },
    },
  },
});

Being recently new to state machines, I'm wondering if this is by design and/or if context could be a parameter to the effect function.
If could be added, I'd love to contribute to it!

I have used xstate a bit and there I can access context within actions and services.
useEffectReducer allows access to context within effects.

Suggestion: Less awkward way to pass context and event types

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 }
  ...
})

Async Guard functions?

trying to use with https://react-hook-form.com/ and make a multi page wizard form.

A guard method seemed ideal to do validation between pages, however the validation function I need to call is async.

Is there any known way to have an async guard?

example
const [state, send] = useStateMachine({ initial: 'name', states: { name: { on: { BACK: { }, NEXT: { target: 'location', guard: async ({ context, event }) => { //ERROR return await trigger(["field1", "field2]); }, } } }, etc

How to use external machine config with correct type (using TypeScript)?

First of all, thanks for this great little library built for React! The machine syntax is really nice and also the TypeScript support. 👍

My problem is that I want to first define the machine config and then use the config object for the hook. Using your sample machine, it would look like this:

const machine = {
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' },
    },
    active: {
      on: { TOGGLE: 'inactive' },
      effect() {
        console.log('Just entered the Active state');
        // Same cleanup pattern as `useEffect`:
        // If you return a function, it will run when exiting the state.
        return () => console.log('Just Left the Active state');
      },
    },
  },
}

const [state, send] = useStateMachine(machine)

The error that I get is:

Argument of type '{ initial: string; states: { inactive: { on: { TOGGLE: string; }; }; active: { on: { TOGGLE: string; }; effect(): () => void; }; }; }' is not assignable to parameter of type 'InferNarrowestObject<Definition<{ initial: string; states: { inactive: { on: { TOGGLE: string; }; }; active: { on: { TOGGLE: string; }; effect(): () => void; }; }; }, { inactive: { on: { TOGGLE: string; }; }; active: { on: { TOGGLE: string; }; effect(): () => void; }; }, undefined, false>>'.
  Types of property 'initial' are incompatible.
    Type 'string' is not assignable to type '"inactive" | "active"'.ts(2345)

I tries the following types without any success:

const machine1: Machine.Definition.Impl = { /* */ }
const machine2: Machine.Definition<unknown, { inactive: 'inactive'; active: 'active' }> = { /* */ }

I also tried to get around the error by definition my own type. But it is incomplete and I don't want to redefine something that already exists somewhere else.

  interface IMachine {
    initial: string
    states: {
      [key: string]: {
        on: { [key: string]: string | { target: string /*guard?: any*/ } }
        effect?: () => void // works, but misses the arguments...
      }
    }  
  }

🤷🏻 How do I solve this problem in a clean way with defining my own types? They are already there. I just cannot figure how to apply them correctly.

Suggestion: Have an explicit initial event?

While typing I was confused why and in what cases the event would be undefined. Only when I was rewriting I realized that the initial event is undefined, which is not that explicit at it's intent.

Libraries often have their initial event, like xstate has "xstate.init", redux has "@@​INIT" afaik, storeon also has "@​init". It's better to have event: { type: "FOO" } | { type: $$initial } rather than have event?: { type: "FOO" }. So I'd suggest there should be an explicit initial event, perhaps a symbol would be good.

DX for typed events

Hey @devanshj, when using types events, I'm getting a confusing error message. Here's a sample case:

1 - I have a types "UPDATE" event that requires a string value
2 - When trying send({ type: "UPDATE" }) it gives me an error (right, it is missing the value)
3 - Problem is, the error says Type '"UPDATE"' is not assignable to type '"some other type"':

Here is an example (the examples/form with this added send on line 57:
Screen Shot 2021-07-28 at 9 49 43 AM

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.