Skip to content
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

Dealing with history #481

Closed
CorentinAndre opened this issue Jun 3, 2019 · 6 comments
Closed

Dealing with history #481

CorentinAndre opened this issue Jun 3, 2019 · 6 comments

Comments

@CorentinAndre
Copy link

Bug or feature request?

Feature request / guidance

Description:

First of all, thanks for the awesome library. It helped our organization manage complex state machines in a React app without redux 💪.

We are having one issue though, and it is about managing history.
Our state machine is a serie of questions, each question might lead to different paths and it has 4 final states. Let's call these 4 states A, B, C, D. There is a total of 5 paths, let's call them PA, PB, PC, PD1, PD2, where every path leads to the letter in its name.
At every state of a path, the user has the possibility to go back to the previous question.

Our problem is, when you are in D, you cannot know for sure which path was the previous. It might be PD1 and you'll be asking a question about the age of the user, or PD2 and the question is totally different.

We don't currently see any way to do this inside the app. Do you have any hint or guidance you can give us on how to manage this kind of situation ?

Thanks 🙂

@davidkpiano
Copy link
Member

Thank you!

History might be a little confusing in this scenario because its use-cases are more statechart-specific; i.e. to mean "return to the previous child state node(s) of this complex parent state node" rather than "return to the previous state". It's for states to recall its last active configuration.

What you can do for now is specify guards on each state that use the previous state.history to determine which state to go to, although it's a bit manual:

states: {
  D1: { on: { NEXT: 'D' } },
  D2: { on: { NEXT: 'D' } },
  D: {
    on: {
      PREV: [
        { target: 'D1', cond: (ctx, e, { state }) => state.history.matches('D1') },
        { target: 'D2', cond: (ctx, e, { state }) => state.history.matches('D2') }
      ]
  },
  // ...
}

If you want to completely swap the current state with the previous state, there's the nuclear option:

let currentState;

service.onTransition(state => currentState = state);

// ...
service.stop();

// restart
service.start(currentState.history);

That will only go one state back in history though (you currently can't go more than one state back in time).

Things get more complicated if you want to allow an arbitrary amount of backtracking, or going forward, or context-specific backtracking. I'll be investigating this area more.

@CorentinAndre
Copy link
Author

CorentinAndre commented Jun 4, 2019

Thanks for your quick reply.

I investigated the use of conditions, but it won't work if for instance you took multiple paths and saved some informations, because at some points both guards can be true but it will only go in the first one.

I'm not sure about how to deal with those edge cases too, what first came in my mind was to have access to the current state inside the assign function, not just the context, and store the StateValue in an array.

Let me know if you have any idea, I might be able to work on this a bit and set up a PR if needed :)

@davidkpiano
Copy link
Member

davidkpiano commented Jun 6, 2019

IMO the best way to do this in XState is using the undo/redo pattern: https://redux.js.org/recipes/implementing-undo-history

const questionsMachine = Machine({
  id: "questions",
  initial: "welcome",
  context: {
    questions, // { one: { ... }, two: { ... } }
    question: "one",

    // history stack
    stack: []
  },
  states: {
    welcome: {
      on: {
        NEXT: "question"
      }
    },
    question: {
      on: {
        NEXT: {
          actions: assign({
            question: (_, e) => e.question,
            stack: ctx => ctx.stack.concat(ctx.question)
          })
        },
        BACK: {
          actions: assign(ctx => {
            const { stack } = ctx;
            const newStack = stack.slice(0, stack.length - 1);
            const prev = stack[stack.length - 1];

            return {
              question: prev,
              stack: newStack
            };
          }),
          cond: ctx => ctx.stack.length > 0
        },
        // ...
      }
    },
    // ...	
  }
});

Instead of treating each question as an individual state, you treat it as a single state of "asking questions" and update the question state through context.

Then, whenever a question is answered, you push that question to a stack.

Here's an example: https://codesandbox.io/s/xstate-react-back-example-4q2vh

@marcelkalveram
Copy link

In case anyone stumbles upon this in the future, I've built a simple wrapper around xState which adds undoable behaviour: https://github.com/marcelkalveram/xstate-undoable. It's not perfect yet and doesn't support redo or interpreted machines, but I thought it might be worth sharing since it's such a common scenario that people run into.

@awreccan
Copy link

https://codesandbox.io/s/xstate-state-machine-with-history-stack-6iq32?file=/src/index.js
^Demo of how to implement history with a back button. My example use case revolves around navigation through "screens" of a user flow through a products screens, like in https://overflow.io/examples/#wireframe-user-flows.

Thanks for this awesome library @davidkpiano, it's a pleasure working with something this well considered. Hope this demo makes sense!

@daniellizik
Copy link

Is there a way to integrate immer patches into this? I'm looking for a way to treat context as "history" (not state nodes), that may be an anti pattern though 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants