qwik icon indicating copy to clipboard operation
qwik copied to clipboard

State machines as a first class concept

Open kee-oth opened this issue 2 years ago • 10 comments

Is your feature request related to a problem?

State management in front end work in general is always a problem. So much of the code developers write creates bugs because of implicit states.

State machines help bring states to the surface by making them explicit. This makes them immediately readable and understandable (and serializable!).

Here's an example XState configuration. You can see the states! :

{
		initial: 'off'
		context: {
			numberOfTimesTurnedOn: 0,
		},
		states: {
			lightOn: {
				on: {
					TOGGLE_LIGHT: {
						target: 'off',
						actions: [
							() => console.log('some side effect')
						]
					},
				}
			},
			lightOff: {
				on: {
					TOGGLE_LIGHT: {
						target: 'on',
						actions: [
							assign((context) => ({ numberOfTimesTurnedOn: context.numberOfTimesTurnedOn + 1 }))
						]
					},
				}
			}
		}
	}

Describe the solution you'd like

The simplest solution is creating an official integration with XState.

But maybe better DX and better performance could be had by a first class state machine solution for Qwik. Maybe some hook like useStateMachine that takes a config object? It could be like a more specific version ofuseStore.

Using XState with TypeScript is also fairly verbose and clunky (they have a typegen CLI which helps somewhat but it's unfortunate to need it).

Describe alternatives you've considered

The alternative to state machines is the predominant way of managing state: implicitly with no guarantees of bug-free code.

Additional context

I believe that first class integration of state machine functionality could be a great thing for adoption of Qwik. Right now, the main draw is the underlying, non-developer facing goodies that Qwik offers (resumability, etc) but with first class state machines, Qwik would get an instant boost in terms of DX since state machines helps developers handle complex business logic. And it'll increase the UX of Qwik apps since state machines help reduce bugs in the code (less errors for the user to encounter)

Here's an article describing the benefits of state machines in a more thorough way:

https://blog.openreplay.com/xstate-the-solution-to-all-your-app-state-problems

Thank you!

kee-oth avatar Aug 29 '22 19:08 kee-oth

Great input. A simple human-readable state machine syntax embedded in HTML would be awesome. Maybe a qwik-city extension like XState (and Xstate Visualizer) that devs can use to plan event flow, state change, and page transitions. This approach helps to reduce application complexity by preventing invalid state transitions (fewer bugs as mentioned before).

Keeping track of the application state is the most undervalued thing in almost any framework.

Thank you!

ChristianHohlfeld avatar Sep 21 '22 23:09 ChristianHohlfeld

Hi @ChristianHohlfeld @kee-oth Thanks for this proposal 🙏 i am not (yet) familiar with xstate but since it is a pure JS/TS implementation it shouldn't be to hard to create the required wrapper for it. Just checked the website and there are already implementations listed we could use as a template to create an official qwik integration 💪 anybody interested in pushing that topic?

zanettin avatar Feb 13 '23 20:02 zanettin

@zanettin

Thanks for the reply! I did try to work on some sort of test integration a few months ago but got blocked from my lack of understanding how to use Qwik properly. I haven't been able to work with Qwik much since then but I'm planning on starting some larger-scale applications with Qwik in the coming months and not having state machines is a blocker to starting those projects for me. I value the safety and ease of logic creation too much to start a frontend project without a way to use state machines.

I think maybe a good way to get the ball rolling is to have someone knowledgeable about how Qwik works to contact someone from the XState team (like Mateusz https://twitter.com/AndaristRake). That's how the XState+SolidJS integration really got rolling. Unfortunately I don't posses enough knowledge of Qwik at this time to make an intelligent integration.

Another integration the Qwik community may be interested in is ZagJS from the ChakraUI team. It's a library of specific machines made for specific pieces of UI:

https://github.com/chakra-ui/zag

kee-oth avatar Feb 13 '23 22:02 kee-oth

Thanks a lot for sharing all the info 🙏 really hope that someone has the knowledge and power to push that topic! totally agree with you that this would be beneficial in many ways 👍 maybe you want to do a quick shout-out at our discord channel? maybe there is smb able to give a hand on this 💪

zanettin avatar Feb 13 '23 22:02 zanettin

Hey guys, awesome proposal. BTW, this week I took some time to implement a simple and naive integration between Qwik and xstate for a project that I'm working on. Here the repo https://github.com/keuller/qwik-xstate.

keuller avatar Feb 24 '23 12:02 keuller

Let me know if I can help out on this as well!

davidkpiano avatar Apr 13 '23 14:04 davidkpiano

I also took some time to integrate my machine with Qwik.

I struggled a bit with serialization at first, but then I managed to extract a custom hook to start the interpreter and subscribe to state updates. Still raw, but it works.

Definition

function useMachine(lazyMachine: QRL<() => AnyStateMachine>) {
  const store = useStore<{
    actor: Nullable<NoSerialize<AnyInterpreter>>;
    state: Nullable<NoSerialize<AnyState>>;
  }>({
    actor: null,
    state: null,
  });

  useVisibleTask$(async () => {
    const machine = await lazyMachine();

    store.actor = noSerialize(interpret(machine).start());

    const subscription = store.actor?.subscribe((state) => {
      store.state = noSerialize(state);
    });

    return () => {
      subscription?.unsubscribe();
      store.actor?.stop();
    };
  });

  return store;
}

Usage

export default component$(() => {
  const chart = useMachine($(() => chartMachine));

  // [...]
});

pleopardi avatar Apr 18 '23 08:04 pleopardi

Made a quick (qwik?) semi-proof-of-concept: https://stackblitz.com/edit/qwik-starter-rknyow?file=src%2Froutes%2Fxstate%2Findex.tsx

Just posting here for reference. Still need to figure out the interpreter part (to run effects)

davidkpiano avatar May 04 '23 18:05 davidkpiano

@davidkpiano I don't use xstate (yet, it does look interesting) but I've been experimenting a bit without xstate, see https://stackblitz.com/edit/qwik-starter-g55vxg?file=src%2Froutes%2Findex.tsx

Your PoC has the problem that it's a singleton and that doesn't work with SSR. To fix that, the machines need to live in context and the problem is that they can't be serialized due to the methods.

Furthermore, for reactivity the state should be a signal or a store.

So the idea is that xstate has configuration and state, and the configuration should be applied lazily when using one of the methods, whereas the state should be serialized.

Ideally, the machine uses a given useStore object to store the machine's state and context, so that it doesn't need to be copied on every transition. Is that possible?

To allow for lazy start/resume, the configuration should be provided as a QRL function, that returns an object that createMachine is called with (or something along those lines). The useRegisterMachine(name, configQRL) hook then stores the name, configQRL, reactive state, and lazy methods in the context.

Then a useMachine('name') hook looks up the machine in the context and returns it. The user reads the state or calls methods. The methods check if the machine was initialized yet and if not they use the registered config to create it and store it wrapped with noSerialize().

(note, the overall context needs to be a regular object, not a store, so that it can be added to from hooks during render)

wmertens avatar May 08 '23 20:05 wmertens

I would love to see official integration here!

engineersamwell avatar Nov 14 '23 19:11 engineersamwell

I would love to see official integration here!

Last time I tried in earnest, it was pretty difficult to do this. I might just need to get more used to Qwik's paradigms, but I'd love for others to give this a try in the meanwhile.

davidkpiano avatar Nov 14 '23 21:11 davidkpiano

@davidkpiano The https://github.com/keuller/qwik-xstate/blob/main/src/routes/index.tsx from @keuller above seems like it ticked many of the boxes @wmertens suggested to do.

engineersamwell avatar Nov 14 '23 22:11 engineersamwell

@keuller nice example with xstate, I think you can do even more when using this trick: https://github.com/BuilderIO/qwik/issues/5087

I don't think you need a visibleTask, you could do a getMachine() call in each accessor method that resumes the machine on-demand?

wmertens avatar Nov 15 '23 13:11 wmertens

we have too many issues and custom serializers (which would enable this) will not be added in the near future so I'm closing this.

wmertens avatar Mar 05 '24 09:03 wmertens

in v2 this will be way easier to do so lets circle back after v2 release

PatrickJS avatar Jun 03 '24 16:06 PatrickJS