Search code examples
reactjsstate-machinexstatexstate-react

XState React - Invoke Service not triggered


I am using xstate with react to implement a basic login functionality. The code is here and the issue I am facing is, on the event AUTHENTICATING it is meant to invoke a service authenticateUser and it is not invoking. No visible errors in the console. The component looks like

import { useMachine } from "@xstate/react";
import { createMachine, assign } from "xstate";
import "./App.css";

const authenticateUserNew = async (c, e) => {
  console.log("service invoked");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        resolve();
      } else {
        reject();
      }
    }, 1000);
  });
};

const loginMachine = createMachine(
  {
    id: "login-machine",
    initial: "unauthenticated",
    context: {
      isAuthenticated: false,
    },
    states: {
      unauthenticated: {
        on: {
          AUTHENTICATING: {
            invoke: {
              id: "authenticateUser",
              src: (c, e) => authenticateUserNew(c, e),
              onDone: {
                target: "authenticated",
                actions: assign({ isAuthenticated: (context, event) => true }),
              },
              onError: {},
            },
          },
        },
      },
      authenticated: {
        on: {
          LOGOUT: {
            target: "unauthenticated",
          },
        },
      },
    },
  },
  {
    services: {
      authenticateUser: () => {
        console.log("service invoked");
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            if (Math.random() > 0.5) {
              resolve();
            } else {
              reject();
            }
          }, 1000);
        });
      },
    },
  }
);

function App() {
  const [current, send] = useMachine(loginMachine);
  return (
    <div className="App">
      <h2>{current.value}</h2>
      <br />
      <h3>
        isAuthenticated: {current.context.isAuthenticated ? "True" : "False"}
      </h3>
      <br />
      <button onClick={() => send("AUTHENTICATING")}>AUTHENTICATE</button>
      <br />
      <button onClick={() => send("LOGOUT")}>LOGOUT</button>
    </div>
  );
}

export default App;

I have tried both the approach where I can externalize function and use it or define it inside the service section of state machine, in both the case it wasn't invoked.

1st approach

invoke: {
  id: "authenticateUser",
  src: (c, e) => authenticateUserNew(c, e),
  onDone: {
    target: "authenticated",
    actions: assign({ isAuthenticated: (context, event) => true }),
  },
  onError: {},
}

2nd approach

invoke: {
  id: "authenticateUser",
  src: "authenticateUser",
  onDone: {
    target: "authenticated",
    actions: assign({ isAuthenticated: (context, event) => true }),
  },
  onError: {},
}

React version: ^17.0.2 xstate: ^4.3.5 @xstate/react: 2.0.1


Solution

  • From docs:

    An invocation is defined in a state node's configuration with the invoke property

    You are instead trying to invoke in an event node, not a state one.

    For example, you could do:

    ...
        states: {
          unauthenticated: {
            on: {
              AUTHENTICATE: {
                target: 'authenticating'
              },
            },
          },
          authenticating: {
            invoke: {
              id: "authenticateUser",
              src: 'authenticateUser',
              onDone: {
                target: "authenticated",
                actions: assign({ isAuthenticated: (context, event) => true }),
              },
              onError: {
                target: 'unauthenticated'
              },
            },
          },
          authenticated: {
            on: {
              LOGOUT: {
                target: "unauthenticated",
              },
            },
          },
        },
    ...
    

    and send the AUTHENTICATE event:

    <button onClick={() => send("AUTHENTICATE")}>AUTHENTICATE</button>
    

    Moreover, I'd like to suggest to avoid the isAuthenticated at all. You can check if you're authenticated with the matches method:

    <h3>
        isAuthenticated: {current.matches('authenticated') ? "True" : "False"}
    </h3>