1

I have two machines: AuthenticationMachine and AddressMachine. AuthenticationMachine is used by Login component while AddressMachine is used by Address component. But I need token from AuthenticationMachine in Address component to be used with the AddressMachine. I've tried spawning an Actor in AuthenticationMachine, but it didn't work.

AuthenticationMachine

const createService = (context: {token: string}) => {
  return Machine({
    id: 'service',
    initial: 'logged_in',
    states: {
      logged_in: {},
      logged_out: {},
    },
  }).withContext(context);
};

const authenticationMachine = Machine(
  {
    id: 'authenticationMachine',
    initial: 'unauthenticated',
    context: {
      token: undefined,
      error: undefined,
      service: undefined,
    },
    states: {
      unauthenticated: {
        on: {
          LOGIN: 'authenticating',
        },
      },
      authenticating: {
        invoke: {
          src: 'login',
          onDone: {
            target: 'loggedIn',
            actions: assign({
              token: (ctx, event: any) => event.data.access_token,
              service: (ctx, event) => {
                return spawn(createService({token: event.data.access_token}));
              },
            }),
          },
          onError: {
            target: 'unauthenticated',
            actions: assign({
              error: (ctx, event: any) => event.data,
            }),
          },
        },
      },
      loggedIn: {
        on: {
          LOGOUT: 'unauthenticated'
        },
      },
    },
  },
);
Joshua
  • 589
  • 1
  • 5
  • 19

1 Answers1

1

I think it might be your use of assign or just the way you implement the components that's the problem.

Here is a working example of your machines in components in a codesandbox

Also, a simple example of how you'd use the machines in components:

Address Component:

function Address(props) {
  const [current] = useService(props.service);
  return <>Address Component token: {current.context.token}</>;
}

Login Component:

export default function Login() {
  const [current, send] = useMachine(authenticationMachine);
  const isUnauthenticated = current.matches("unauthenticated");
  const isAuthenticating = current.matches("authenticating");
  const isLoggedIn = current.matches("loggedIn");

  if (isUnauthenticated) {
    return (
      <>
        token: {current.context.token}
        <br />
        <button onClick={() => send("LOGIN")}>Login</button>
      </>
    );
  }
  if (isAuthenticating) {
    return <>...hold on, authenticating</>;
  }
  if (isLoggedIn) {
    return (
      <>
        token: {current.context.token}
        <br />
        <button onClick={() => send("LOGOUT")}>Logout</button>
        <br />
        <Address service={current.context.service} />
      </>
    );
  }
}

Your machine example expanded:

import { assign, Machine, spawn } from "xstate";

const createService = (context: { token: string }) => {
    return Machine({
        id: 'service',
        initial: 'logged_in',
        states: {
            logged_in: {},
            logged_out: {},
        },
    }).withContext(context);
};

const login = (ctx) => {
    const { email, pw } = ctx;
    return new Promise((resolve, reject) => {
        if(email === 'a@b.c' && pw === 'abc') {
            return resolve({ token: 'jahsfjkhakjsnfj'});
        } else {
            return reject({ token: undefined });
        }
    });
};

const authenticationMachine = Machine(
    {
        id: 'authenticationMachine',
        initial: 'unauthenticated',
        context: {
            token: undefined,
            error: undefined,
            service: undefined,
            email: 'a@b.c',
            pw: 'abc'
        },
        states: {
            unauthenticated: {
                on: {
                    LOGIN: 'authenticating',
                },
            },
            authenticating: {
                invoke: {
                    id: 'login',
                    src: context => login(context),
                    onDone: {
                        target: 'loggedIn',
                        actions: [
                            assign({ token: (_, event) => event.data.token }),
                            assign({ service: (_, event) => {
                                return spawn(createService({token: event.data.token}));
                            }})
                        ]
                    },
                    onError: { target: 'unauthenticated' },
                },
            },
            loggedIn: {
                on: {
                    LOGOUT: 'unauthenticated'
                },
            },
        },
    }
);

export { authenticationMachine };
TameBadger
  • 1,580
  • 11
  • 15
  • I thought about using that approach to the problem. I've tried that approach with a react project and it worked fine, but I'm using react native and react navigation. I can't pass props. – Joshua Dec 04 '19 at 17:01
  • what about using screenProps like detailed in this post, https://medium.com/@benfox_63249/share-state-between-screens-with-custom-navigators-in-react-navigation-62a34e3c7f97 ? – TameBadger Dec 05 '19 at 10:18
  • I wouldn't be able to help that much more as I don't really have experience with react-native – TameBadger Dec 05 '19 at 10:19
  • The react navigation router doesn't accept child routes like you would on the web, it provides a function e.g createStackNavigator function that returns - I guess a component. I've tried some different approaches that I found online to pass screenProps, but the props weren't getting added. – Joshua Dec 05 '19 at 14:16