2

I am trying to persist state using Firebase. I am currently using a 'saveState' function which works fine and properly saves the most recent state to Firebase.

Now I'd like to be able to initialize the state machine based on the most recent saved state in Firebase. In the code below I am trying to use my 'loadState' function to provide Xstate with a config object. It currently returns a promise with the correct state configuration within.

Here is my 'saveState' code:

 //This function works fine.
 function saveState(current, id){
        let transactionJSON = serialize(current);
        transactionJSON['createdOn'] = new Date();
        return firebase.saveTransactionState({transactionJSON, transactionId:id});
    }

Here is my 'loadState' function which returns a promise from Firebase with the correct config information within.

function loadState(id){
        return firebase.getTransactionState({transactionId:id}).then(function(querySnapshot) {
            return querySnapshot.docs.map(doc => deserialize({...doc.data()})  );
        });
    };

Now my issue is trying to load Xstate with the above 'loadState' function. Here I am trying to use a useMachine React hook:

const [current, send] = useMachine(transactionMachine,{
        state: () => loadState(id), //Trying to do something like this, but it doesn't seem to work.
        actions:{
            save: () => saveState(current, id),
        },
    });

I end up with the error: "TypeError: Cannot read property 'data' of undefined", which I believe is happening because the promise hasn't resolved yet leading to trying to read an undefined value.

Is this even possible or am I going about this all wrong?

I am new to all this, any guidance would be appreciated. Thank you.

uponly
  • 43
  • 1
  • 7
  • Same here, anyone knows the answer – Mobin Samani Mar 09 '21 at 05:09
  • 2
    You can't initialise state with a value that's fetched asynchronously, cos Javascript is single threaded so if you somehow waited for that value to come back from Firebase your entire application would be completely frozen until it came back - no good. Just give some kind of falsy or default initial state and handle that in your React code, and render a loading spinner/message or just conditionally render the component based on whether the data has been fetched yet or not. – Jayce444 Mar 09 '21 at 05:11
  • @Jayce444, Yeah, thanks for your comment. I just thought that there might be some cool way to do it with Xstate react hooks (because I may not be understanding their docs correctly), seeing as though promises are state machines in and of themselves. – uponly Mar 10 '21 at 19:56

1 Answers1

0

I suppose you could use useEffect to update your state machine following your fetch.

How about invoking the fetch within the state machine?


import { Machine, assign } from 'xstate';


const yourMachine = Machine({

    id: 'yourStateMachine',
    initial: 'fetching',
    context: {
        values: {
            id: '', // likely need a state to initialize this value before fetching
            apiDat: {}
        },
    },
    states: {

        fetching: {
            invoke: {
                src: ({values}) => firebase
                    .getTransactionState({ transactionId: values.id })
                    .then(function(querySnapshot) {
                        return querySnapshot.docs.map(
                            doc => deserialize({...doc.data()})  
                        );
                    }),
                onDone: { target: 'resolving', actions: 'cacheApiDat' },
                onError: { target: 'error.fetchFailed' },
            }
        },

        resolving: {
            initial: 'delay',
            states: {
                delay: { 
                    after: { 
                        750: 'resolve' 
                    }, 
                },
                resolve: {
                    always: [
                        { cond: 'isSuccess', target: '#yourStateMachine.idle' },
                        { cond: 'isUnauthorized', target: '#yourStateMachine.error.auth' },
                    ],
                }
            },
        },

        error: {
            states: {
                fetchFailed: {
                    type: 'final',
                },
                auth: {
                    type: 'final',
                }
            }
        },

        idle: {

        },

    }



},{

    actions: {

         cacheApiDat: assign({ values: ({values}, event) => ({
                   ...values,
                   apiDat: event.data, // save whatever values you need here
              }),
         }),

    },

    guards: {
        // this requires a definition based on firebase's returned api call on a success
        isSuccess: ({values}) => typeof values.apiDat.data !== 'undefined',
        // this requires a definition based on firebase's returned api call when the call is unauthorized
        isUnauthorized: ({values}) => typeof values.apiDat.detail !== 'undefined' && values.apiDat.detail === 'invalid_auth',
    }

});

Harley Lang
  • 2,063
  • 2
  • 10
  • 26
  • Thank you for your reply. This would be nice except I am not sure how I can bring in the firebase context into the state machine directly because I have the state machine in a separate file. I use `const {firebase} = useContext(FirebaseContext);` to get the firebase context and access the `.getTransactionState` function. I wonder if there is another way to pass in the firebase context directly into the state machine? When I try to define firebase in my state machine file, I understandably get 'React Hook "useContext" cannot be called at the top level.' – uponly Mar 28 '21 at 19:04
  • You can create an 'init' state on the state machine and setup a value `firebase` in the state machine context. Next, when the component initializes, you create a `useEffect` that will check your `firebase` value in the state machine. If it is not set, you update it from the `useContext` call you described. Now that the state machine has your `firebase` value, transition to fetching! – Harley Lang Mar 28 '21 at 19:21
  • Would you be able to clarify 'when the component initializes, you create a useEffect that will check your firebase value in the state machine'? I am trying to implement your code, but I am using `asEffect` in my `useMachine` hook like: `actions: {load: asEffect((context, event)=>{context.firebase = firebase; context.transactionId = loadedState.id;} })}`. I am now getting 'TypeError: Cannot read property 'getTransactionState' of null', presumeably because my statemachine is running before my hook accesses it. – uponly Mar 30 '21 at 03:44
  • I forgot to use the `assign()` function, but I am still getting the same error. I forgot to mention that I am also doing `entry: 'load'` in the fetching state, alongside all your code. – uponly Mar 30 '21 at 03:56
  • Oh, so 'load' works as your initial state. In your react component, you want to use `useEffect`, not in your state machine. Essentially, your component will render and the initial state will be `load` with one event: `on: { LOAD: { actions: 'loadContext', target: 'fetching' } }`, your react component `useEffect` will check to see if your machine is in `load`, the `loadContext` action will save your firebase values to the state machine's context, and you should be in business. If this is gibberish, let me know and I'll try to throw together a sandbox to demonstrate in the next day or two. – Harley Lang Mar 30 '21 at 04:46
  • 1
    That would be great. Actually, I put together a small sandbox based on my code and your suggestions to help show what I mean. Hopefully, it helps. Feel free to change anything you need. Basically on App.js line 30 is where I am trying to send the firebase context into my stateMachine.js file. https://codesandbox.io/s/crazy-mendel-te0dl?file=/src/App.js – uponly Apr 02 '21 at 18:24
  • Awesome thanks @uponly! I will take a look at this some time later today and get back to you with some edits :D – Harley Lang Apr 02 '21 at 19:52
  • On a first glance, you are on the right track. I would change your state machine such that (1) you are injecting the firebase into context while the state machine is an `initialize` state (e.g., `useEffect(() => { if (machine.value === 'init') { setMachine('LOAD', { firebase: firebase }) }}, [])` in your react component, then (2) setup that initialize state in your state machine so that the `LOAD` event assigns the passed firebase value to the state machine's context, and THEN (3) transition to loading (because at that point, `context.firebase` is no longer null. – Harley Lang Apr 03 '21 at 15:33
  • 1
    Sorry for the very late reply. I didn't really get it to work, for now, I ended up doing what @Jayce444 mentioned. I fetched the `machineConfig` from firebase in a parent component and passed the result as a prop to the child component (where I am using `useMachine` hook). There I check if `machineConfig` prop exists, if it does, send the current state back up with `useEffect` to the parent component via a `setState` hook that was also passed down to the child component. If `machineConfig` doesn't exist, initialize to the machine's initial state. I'll also post an answer soon. – uponly May 01 '21 at 19:56
  • Cool! I look forward to reading your solution :) – Harley Lang May 01 '21 at 22:26