Multistep form handling in React Native with Finite State Machines, Formik and TypeScript
Multistep form handling in React Native with Finite State Machines, Formik and TypeScript
React Native, XState, Formik and Yup - handling a multistep form with these tools seems really interesting and fun. If you are wondering how one can utilize them in their own mobile app, give it a read!
Lately, I had a chance to work on a mobile app that utilizes this concept of finite state machines. Having no prior knowledge on such thing as a state machine, I quickly got to liking it!
Basically, the concept of such state machines is that we have a finite number of states and we can transition between these states while also exchanging some data. We can declare multiple machines which can handle different tasks and behave in a different way, as well as declare some universal ones that can be re-used. That being said, we can think of our app as a combination of state machines that, based on their states, render different parts of UI. For example, fetching data from the back-end - we are either in a fetching state (we can render some kind of a loader at this time) or in a done state where we can display all the fetched data along with some kind of information about our request status, e.g. whether it was successful or not.
If you want to start developing an app based on state machines, there’s this cool library called XState - the one that I used in the aforementioned project and got familiar with (but not entirely, at least yet! 🙂).
If you want to read more about it, and about finite states machines concept in general, go here: XState
In this article, I will try to share some knowledge about it with you. We will develop a simple React Native app, which handles multi-step forms. Apart from XState, we will use Formik and TypeScript.
Part 0 - Our case
What we want to achieve and how?
We want to:
have the user update their address, billing and contact information
check what information is not yet updated and based on this redirect the user to the appropriate form step (screen)
save progress as the user goes between each form step (when a user decides to complete the form later, the app will redirect them to the place when they left off)
How?
Back-end wise, let’s imagine that we have these two endpoints - one for getting the user data and the second one for updating (e.g. https://myapi.com/api/user with GET and PUT methods).
For the sake of this article, we will use just two simple functions instead of calling the real API. In a real-life scenario just exchange these functions with fetch or axios.
As you might have already noticed, we have to call the back-end multiple times while updating (each step will update the user - remember, we want to save progress as we go. We could, of course, save all the data locally and send the request at the end but you know, saving progress, yay! 🔥)
So in order to DRY, we can declare the state machine that takes in the previous userData and handles BE calls (in our case just mock functions with timers). We can then reuse it for every screen.
Apart from declaring this machine, we will also create another one: a parent, which states will represent every screen. We will also use it to handle redirecting.
Final app
Part 1 - XState machines and Typescript
Let’s get our hands dirty already 😎!
Before we dive into the VS Code (or whatever IDE you will be using), let’s use this cool Xstate Visualizer and try to initially declare our machines.
When we visit this page, we will see this graphical visualization of example machine:
As we can see, we have 4 possible states that the machine can be in + a bunch of different events, thanks to which we can transition between states. Notice that:
Some of the states can be e.g. final (we cannot transition somewhere else when we get into this state + we can pass the data back to the parent at this point, but more on this later on)
We can transition only between given states with a given set of events. We won’t be able to transition e.g. from idle to failure.
That’s the beauty of state machines 💪🏽🔥!
Ok! So now, that we know all the basics, let’s try to declare our first machine -> the one that will be responsible for updating the user.
Re-usable child machine 👷🏼♀
Possible states that can represent all the necessary actions that need to be done while editing the user data on one particular screen:
fetch -> we will fetch the latest user data, just to be sure and up to date with back-end
edit -> when in this state, we will be able to input new data / edit previous. Also, this will be the point when we are transitioned back to when error occurs
pending -> we will transition to this state after being done with editing the inputs. Back-end call will be made
done -> we will transition to this state when the updating is successful. Also, this is the final state and will indicate that we can either go and update another part of our form or just redirect the user to some kind of a success indication screen/page.
With code (for now just JavaScript), it would look like this:
Visualization (you can click through all possible states/events):
There is one more thing that we have to take care of! We should declare the machine context -> data, that can be updated and used within machine states. With our updateMachine, we would want to have access to all the user information fields as well as something like error and errorMsg. Let’s add it to our machine:
Now that we have our updateMachine ready, let’s do the same with the parent one (this parent will invoke the previous child machine in every state that represents given form part. More on that later)!
Parent machine ⬆️
Let’s start with all possible states. I decided to divide my form into these steps:
init -> first state in which we will initially check the user data and conditionally redirect the user to a specific screen
basic -> state representing the form part for updating first/last name, e-mail and phone data
address -> updating address data: street, city, post code and country
payment -> updating all necessary payment data: account number, credit card number, credit card expiration date and CVV code
complete -> state that we will be redirected to when all the user data is present and filled :). For different reasons, this one will not be final (more on this later on)
Events would be somehow similar to the previous machine, except instead of ERROR, we can use something like BACK. Makes sense, doesn’t it?
Wait…
What about when we want to redirect from init to let’s say payment because that is the only data that the user did not update yet (after e.g. exiting the app with a plan to finish it the next day or something like that). Well, we can declare some more events:
BASIC -> will redirect us to basic state
ADDRESS -> will redirect us to address state
PAYMENT -> will redirect us to payment state
So with everything together, we end up with this setup:
As for the context, we will reuse the code from the child machine. Later on, when declaring our machines in the code, we could make this a shared context variable and init our machines with it :)
As we can see, initially, we will check what information about the user is lacking. Based on this information, we will redirect the user to a given form part. While being in one of the form states, we can go between each one of them, and after updating all the information, we will be redirected to the complete state.
What about TypeScript? 💎
As stated in the introduction to this article, we will use TypeScript to write our app, thus we also need to add some types, yay!
Let’s start again with our child machine: updateMachine.
As you would guess, we have 3 things to cover with types:
machine context
machine states
machine events
For the context, we simply do it with an interface, just like so:
With events, we can use an enum again, representing every possible event, and then we will extend an EventObject that we import from xstate library (more about that later on)
WE ARE DONE with these types at last! Quite a lot of work and we did not even start coding the actual app, but it is worth the time, you’ll see. Especially, when we work with multiple machines context/states at the same time! 😀
If you don’t want to create your own project from scratch, I have setup a simple hello world app in React Native with UI Kitten library (for our UI), declared UserData interface for our users’ data and two functions to mock our back-end calls (see: Checkpoint #1).
Don’t forget to run these commands if you are starting out with this starter repo:
yarn
and then
cd ios && pod install && cd ..
Mocking the back-end
To start with, here are our two functions that will mock back-end calls:
src/data/Api.ts:
export const getUser = async (prevUser?: UserData) => {
console.log("Pending...")
const scenario = getRandomNumber(1, 4)
await new Promise(res => setTimeout(res, 1000))
if (prevUser) {
return prevUser
} else {
switch (scenario) {
case 1:
return userEmpty
case 2:
return userWithContact
case 3:
return userWithAddress
case 4:
return userComplete
default:
return userEmpty
}
}
}
Depending on whether we provide the prevUser as an argument to this function or not, it will return either the same thing as provided, or generate a random user response. userEmpty / userWithAddress etc are just objects of UserData type, e.g.:
Each screen that contains particular parts of the form, at least in our case, will be basically the same. The only things that will change will be Inputs and actions assigned to each button (prev and next button) thus in order not to repeat ourselves, we can create some kind of a wrapper component for our form 😀
Let’s navigate back into our root directory and:
mkdir src/components
and
touch src/components/FormWrapper.tsx.
Our FormWrapper component will render the screen’s title, its children (being different inputs) and buttons that will trigger different actions. Actions will be passed to it as props. Also, its content is wrapped with KeyboardAvoidingView so we are sure that upon focusing InputField the keyboard will not cover it.
Now, that we have our FormWrapper being ready, we can go back and add inputs in each screen. I divided the form into 3 parts -> Name, Address and Payment. Let’s create them:
In the first step (Name and contact screen), we want to have four inputs:
first name field
last name field
e-mail field
phone field
With our wrapper, the output component would look like this:
We used the useNavigation hook in order to have access to navigate and goBack methods.
Do the same with the rest of the screens so that every field from UserData interface is represented by an input in one of these 3 screens.
Apart from the inputs, we will also add some kind of success screen and home screen (Home.tsx and Success.tsx files we created earlier). For now, our home screen can be just a simple screen with a button that will redirect us to the first form screen:
In these newly created files, let’s add our types for each machine - just copy and paste everything that we have done in the previous part #1 XState machines and Typescript.
We will start with updateMachine - a child one, invoked inside every parent machine’s state representing a given form part.
To start with, we need to import the Machine object from xstate library.
import {Machine} from 'xstate'.
Then, we can instantiate our machine, just like so:
export const updateMachine = Machine()
This new machine takes an object as a parameter with the initial machine configuration. What it means is that we should pass to it all the states, initial context, id, transitions, etc. Apart from that, we can additionally (thanks to TypeScript) add types to this new machine, types that we just have added to our code a moment ago.
Now, along with our Machine configuration, we can also pass all the declared types for context, state and events:
We assigned an id for this machine, added initial context to it and defined all the states. If we try, for example, to add one more state that is not declared in our type, we should get a warning informing us that this new, additional state does not exist in the type that we defined earlier 😀
Next, we can also add all the transitions between all the steps. We can do it like so:
These few lines that we have just added tell us that whenever we send an event of type ‘NEXT’ to the machine that is currently in the fetch state, we will be transitioned to another state -> edit.
Having added all the remaining possible “moves”, our machine states object will look somewhere the same as below:
Notice that the done state is of type final. When our machine reaches this point, we will be able to perform some kind of an action that is available for that type 😀. More on this later on. Keep reading! 💪🏽
At this point, right off the bat, we can also update the machine context while transitioning with a little help of assign* (an action used to update the machine’s context). This action**, while updating the context, has access to all the event data that we pass when transitioning + access to the current context thus we can either update the context based on the previous one (think of useState(prev=>prev+1) for example 😀), or use the data passed with an event.
It takes the context “assigner”, which represents how values in the current context should be assigned.
* more in-depth look on assign in XState docs: Assign Action
** simply put, actions are these side-effects, that can be invoked/triggered when e.g. entering / exiting the state or when transitioning. You can read more about them here: Actions
It is highly possible, that when reading the previous paragraph, you got a little bit confused when I mentioned event data. (If not, you rock! You can skip the next paragraph).
Event data?
With xstate, we can invoke different actions, for example call the backend. Our call can be successful or not. Based on this information, we transition either the one or the other way. To transition, we have to tell the machine what type of an event we want to send to it (we were declaring this before -> e.g. NEXT). Apart from this information, we can also pass whatever we want, for example the response data from our back-end call 😀
Back to the assign
For now, let’s handle only the error part of our context. We simply want to update our context with new error value after every transition. The only time, that the error field should be set to true is when we transition with ERROR event type. So on every transition with ERROR, we set error to true, while with transitioning with NEXT, we reset this value back to false. We can then use this to e.g. display some kind of an error pop-up for the user
This way, we make sure that whenever we transition with NEXT, we go back to “no-error” scenario. Let’s do the same in the other direction - whenever we transition with ERROR, we set the error value to true. For now, we will also set the errorMsg to “Error” (will change it later though 😀)
Now, that we know how to update the context, let’s handle the scenario when we’re in different states and want to “do something”. Every single state has access to this invoke property. This value is just an object, with which we can tell what we want to “invoke”, with what data etc. ((in-depth look: click here)). We can invoke a Promise or a different machine (will do this soon 🔥), or a “callback handler”. I especially like the latter 😀.
When invoking something, we have access to three things (basically four, when we want to listen to the parent machine, but let’s stick with three for now):
Our machine context -> src: (context, ...
Our event data (similarily to assign) -> src: (context, event) => ...
Our callback handler -> => async cb =>
This “callback handler” (cb) is just a function that we can call with the event data. In other words, when our “action” (the one that we invoke in src property) is finished with success, we can call this cb() with data about:
a type of an event (based on this information we will be transitioned to a different state) -> cb({type: UpdateEvents.NEXT});
and other data (response from back-end call), that later we can use e.g. with an assign :muscle:! -> cb({type: UpdateEvents.NEXT, hello: 'world'});.
To summarize, in the previous code snippet, we have invoked a function that after 2 seconds will transition us to a different state with an event of type NEXT and some additional data in the form of a simple message.
Let’s finish adding all the remaining actions stuff to our machine:
We add actions with invoke in two places: in fetch state (when we fetch/update user data) and in pending state (when we send the edited data to the back end to update the user). For now, we are using timeouts to mock the back end calls but later on we will replace it with our logic.
Our machine should be working now, so let’s quickly test it in our app (we will use useMachine hook to quickly test everything and afterward we will delete it and get back to setting it up properly later on 😀)
Test-drive our updateMachine
So let’s quickly test this machine with useMachine hook inside e.g. FormName component. Remember, it is just for the sake of testing whether this machine is setup correctly. We can get rid of the stuff that we code in this sub-part completely after we make sure it works.
Let’s add the hook (it returns an array of values that we can destructure - the first element, current*, tells us about, among others, the machine current state / context and the second value, send*, is a function that can trigger transitions):
*we can name it whatever we want, just to be clear, it does not have to be current or send, we can name it e.g. machineData and sendEvent etc
const [current, send] = useMachine(updateMachine)
And let’s use useEffect hook to console.log all the changes + trigger our first transition:
* useMachine hook - first element from an array returned by the hook has this cool method .matches. Thanks to it we can check if the machine is currently in a given state.
current.matches("TEST_STATE") -> this code will check if the machine is in the TEST_STATE state. If so, the returned value from this method will be true. Else, false.
Our console output should look like this:
LOG current state
LOG fetch
LOG current context
LOG {"error": false, "errorMsg": "", "userData": null}
LOG current state
LOG edit
LOG current context
LOG {"error": false, "errorMsg": "", "userData": null}
How it looks in the app:
Our machine was first in the fetch state, and then, after transitioning with NEXT event type went straight into the edit state. While fetching, we displayed some text (see the pic above).
Now, let’s delete all of this test code from our FormName screen 😂 -> our machine is working so we can now proceed with our work.
userDataMachine
It’s time to take care of our main, parent machine ⬆️ which will invoke the previous machine as child services in every state that is representing a given form part.
Let’s do the same as with the previous one -> declare a new Machine with a configuration object with initial context, transitions and types.
This is how it should look like, at least for now:
As mentioned at the very beginning of this article, and also a little bit later on, this parent machine is supposed to represent every form screen, thus our states are named e.g. “address”, “payment” etc.
The very first state, “init”, also is representing the screen -> this can be a loading screen displaying to the user e.g. the message that the app is checking whether we need to update some profile data or not. Based on this information we will be redirected to one of the form screens.
So now that we have all the states and transitions setup, we can actually connect it with our navigation.
As the first step, we need to wrap our existing navigator with another navigator. Why?
We have to have access to navigation props inside the component with our “form” navigator
The app that we are building is supposed to be a part of some kind of a bigger app -> for example, after logging in, we would check whether our profile data is completed or not. Depending on this information, we can either navigate to the main dashboard of the app, or to the screen with form, being in fact another navigator (our FormFlow).
Let’s do a little make-over in our App.tsx and create another StackNavigator.
...
const Stack = createStackNavigator(); // already used navigator
const Root = createStackNavigator(); // new one
...
Now, let’s move our existing navigator inside a separate component and render it as a screen of the new navigator:
Now, if we were to add some kind of a “listener” for our machine changes, we could map every state to each screen with switch statement and fire up the navigate method from our navigation prop to jump between screens. Let’s try to implement it.
We can use useNavigation hook to get access to the navigation property with all the necessary methods:
As the purpose of this timeout was to only test the transitions, we can freely delete it now.
Fetching initial user data and checking where to redirect them
We can now start fetching the user data in our parent machine! Let’s use invoke property of our init state, and call for the user data with our mock function that we need to import (in real-life scenario it should be fetch, axios or whatever, this mock is only for the sake of this article):
Now when we reload the app, we should see nothing in particular to be changed, but when we look into our console, we should get a log of the fetched user data.
Now, depending on the user fields and whether they are null or not, we can update our context and redirect the user to the appropriate screen with our callback handler.
Along with type of an event, we also pass our userData to the callback handler. So let’s assign it to our context on transitioning between the appropriate states:
Blocking the gestures and Android hardware back button
Let’s imagine, that we are redirected to the payment form. When we use the hardware android back button or swipe from left on iOS, we will be redirected to the previous screen in the stack. What was our previous screen? The initial one, where instead it should have been the previous form part screen. It can happen for example, when the user starts updating their profile from scratch, and then, in the middle, decides to kill the app and finish it later. Upon the second try, the user will be redirected to the place where they left off, but when they want to go back (either by swiping on iOS, or by using hardware back button on Android or back button in the form) they will be transfered back to the first screen. To prevent that, we will:
Disable swipe gestures for iOS
Assign custom back functionality for Back buttons in the form screens
Add a custom behavior for hardware Android back button.
First one is fairly easy and not that much complicated. We just have to edit the navigator options on App.tsx:
We are done for iOS. In order to edit the android back button behavior, we will use useFocusEffect hook provided by react-navigation library. It will trigger some actions whenever we focus given screen. Next, we will have to import the BackHandler object from react-native and
block the default action and
add custom one that sends the correct event to the machine
To disable the default back button behavior, we need to pass a function to our BackHandler listener that returns a boolean. When we return true, the default behavior will be blocked. Every additional custom action can be invoked in the body of this function (e.g. goBack in our case).
goBack is a function that we will use for navigating/sending events to parent machine. Let’s declare it along with goNext (you know for what)
These two functions, as well as customizing useFocusEffect with BackHandler handling, should be added inside a component that has our navigator inside.
Changing the way we navigate between screens
Now, we need to pass down some new things to our form screens -> functions for navigating (goBack and goNext) and also invoked child machines (we did not invoked them yet inside our machines configs. More on that later on in the article)
Let’s change the way that we pass our components to screens.
If you are observant enough, you might have noticed, that we passed the goBack function to the FormName screen. We don’t want to be able to go from this screen back to the initial, Home one. But don’t worry, thanks to the way of how we have setup our transitions between states, when we send the BACK event while being in FormName state, the machine will not change its state as this kind of transition is not possible. You can try it yourself 😃
Invoking child machines
Now, we can finally invoke appropriate stuff inside every form state, that being invoking our updateMachine inside different userDataMachine states.
Remember how we can invoke some actions when we’re in a given state with src property of invoke object?
Quick reminder:
...
STATE: {
invoke: {
src: ...
}
}
...
Earlier we used invoke property to take advantage of callback handler. Now, we want to simply pass our child machine as a source (src) to the invoke property. Along with invoking another machine, we can pass some data to it, as well as give it a custom id (which is important, you’ll see why later).
Let’s do so then. We will setup the invoke object for the FormName form part (basic state in our parent machine) first.
We give it an ID for the child, which will be used to get this invoked service working as a child machine using useService (similar to useMachie) hook later on. Then, we pass our child machine as src. Right after, we pass the context in data property and then we can handle the child machine being done in the onDone property. We will handle it a bit later on. For now, it is good to know that the stuff inside this property will be triggered as soon as the child machine enters the state which is final 😃
Let’s do the same for all the other states: UserDataStates.address and UserDataStates.payment. Don’t forget to give each one of them an unique ID.
Now goes the question:
How can I get access to this invoked child machine and this updated context? Do I use useMachine hook? Or useService hook mentioned in the previous paragraph? If so, what should I pass to it, just this updateMachine? Will it even work?
Here’s the answer:
This invoked child machine is somehow a part of our parent machine (userDataMachine), thus in order to get access to all invoked services/child machines, we simply need to extract them from the parent.
When we used this useMachine hook earlier, we destructured only two elements from the array that this hook returns:
current (or whatever you called it) -> 1st element that gives us information about states of the machine, context, current state and more
send (or whatever you called it) -> 2nd element in the returned array that is a function used for sending events down to our machine.
There is also a third element that we can destructure from the array returned by the useMachine hook -> services (again, how you name the destructured element is up to you 😃).
Thanks to this destructured element, we will get access to all the children of the machine for example. We can simply pass this to every screen, then get the appropriate child (by ID, thus it was so important to give each invoked child one 😃) and pass it to our useService hook (works similar to useMachine. The difference is that we pass a child machine to it as a parameter not a machine object) 😃
Let’s start then by extracting this third element (service) and pass it down to our screens.
In our “app root” being App.tsx, let’s update this part of the code:
...
const {navigate} = nav;
const [current, send, service] = useMachine(userDataMachine);
// destructure also the 3rd element from the array returned by the useMachine hook
useEffect(() => {
...
Inside each screen, we render the component only when the machine is in the appropriate state, thus the child is correctly invoked and we don’t get the undefined variable error kinda crash of the app. When we try to render the screen without this current.matches() checking, we could end up with the app telling us that the child that we want to extract by given id from service object is undefined.
Then, inside each screen that has this service passed to it, we can extract the invoked machine, and pass it to the new useMachine hook, like so:
Interpreter is imported from @xstate/react and we pass to it the context and events types for our parent machine.
With service.children.get(ID) we are able to get the invoked child.
Remember when I told you that this machine that we extract from the service will be used with useService similarily to the useMachine hook? This is the exact time we do that. Import it from @xstate/react.
For our usecase, apart from the name, this hook works, let’s say, exactly the same as the useMachine one.
Given that we succesfully passed the service to every screen and invoked a new machine with useService in every screen, we should get this hello from child machine, here's the passed context string printed out in the console when we cycle through each form screen + the machine context:
Get rid of the navigation button in our Home screen (Home.tsx) that uses the navigation props to go to the next screen. Instead of the button, let’s put the loader there 😃
Change the transitions between screens and add cardStyles (for there’s no white background behind the screens)
Reset navigation stack so every animation transition is done the same way
Update our switch statement responsible for redirecting using above code as a schema. With the previous version of react-navigation we could use SwitchNavigator. Now we can either do it this way, or render the screens inside navigator like so React Navigation Docs - Switch Navigator - upgrading from 4.x
How it looks so far:
Handling forms (with Formik and Yup)
If you want to start working from this part, here are the files Checkpoint #4
In this part, we will finally handle our forms and inputs and add some simple validation. Validation wise, you would want to go for something more thorough than just checking whether the input value is present or contains e.g. only numbers.
In each screen, the moment we initialize a new machine/service, we get access to the latest user data. Let’s pass this data as initial values to our inputs. As we want to use Formik and Yup* in order to handle the forms, we have to configure both of these libraries first.
* we have installed these two packages at the very beginning of an article. If by any chance you don’t have it installed, do it now 😀
Let’s start with importing useFormik hook from formik package and initializing it inside our screen components (each screen should have its own formik configuration). Screen with “name form” is the first one in the order, so let’s start with it 😃
Apart from passing the initial values, we also tell formik to validate the values on every input change and on blur (meaning loosing focus on the currently focused input).
But how come formik knows when the input value is valid and when it is not? It’s thanks to the validationSchema that we also need to provide to the configuration object. In effect, formik will know how to check and validate the values.
To define the validationSchema, we can use Yup library that we also should have installed now (if not, add it with yarn add yup).
We will need to define one schema for each one of the screen forms.
To do so, we simply create a new variable with Yup.object().shape(config) and then pass it to our formik config object.
Our schema:
const FormSchema = Yup.object().shape({
name: Yup.string().required("Required"),
surname: Yup.string().required("Required"),
email: Yup.string().required("Required").email("Should be an e-mail"),
phone: Yup.string()
.required("Required")
.matches(new RegExp(/^[0-9\s]*$/), "Only numbers and spaces allowed"),
})
As for the validation options, we have plenty to choose from. For detailed descriptions you can visit the Yup docs here.
As said before, we just want some simple validation, only to check whether the inputs are not empty, are e-mail or if they are numbers-only.
Now that we have our schema ready, let’s pass it to formik:
We can also tell formik what it should do on submit. When we try to invoke the submit with formik while the values are invalid, formik simply won’t do anything. Our submit function will not take action.
To add submit behavior, it is just a matter of passing a function to another config property of formik config object:
On submit, we will send the event type to our machine, along with newly updated values.
Let’s quickly go back to our child machine to address this “new thing” that we want to do with this line of code: send(UpdateEvents.NEXT, values); inside onSubmit in formik configuration.
After navigating to updateMachine.ts and to the part where we have the configuration of UpdateStates.edit state, we should see something like this:
We tell the machine that it should navigate to UpdateStates.pending upon the NEXT event. Let’s update this behavior by assigning new data to the machine context.
When we call this on submit (with Formik):
send(UpdateEvents.NEXT, values)
We pass new values as event data along with the type of an event (the type being NEXT).
To update the context on transition, we can do something like this:
We assign a new object to the userData in our machine context with a little help of spread operators, made of the previous context data (this destructured userData form the first argument) and the newly updated values from event data (second argument).
Now, whenever we invoke the submit action with formik, an event UpdatedEvents.NEXT will be sent to our machine (along with new user data). It will transition us to the next state (pending) along with updated context. While pending, we will call the back-end to update the user (in our case - just a mock function) and the updated user from the response will be passed one more time to the next transition.
When our machine finds itself in the pending state, it will have access to the latest userData from the context. We will use it to update the user in the backend.
Let’s also edit the behavior in pending state then:
And now let’s replace this timer thingy with our mock back-end call function:
...
invoke: {
src: ({userData}) => async cb => {
try {
// CALLING BACK END TO UPDATE USER
if (userData) {
const response = await updateUser(userData);
cb({
type: UpdateEvents.NEXT,
userData: response,
});
} else {
throw Error('User Data is null');
}
} catch (e) {
cb({type: UpdateEvents.ERROR});
}
},
},
...
When the back-end call is successful, we will be redirected to the final state. When a machine, which is invoked inside another machine, reaches its final state, we will be able to invoke an action in our parent in this very moment and also get access to some data from our child.
That being said, when we reach the final state in our child machine, updateMachine, its context has the latest userData. This data can be passed to the parent.
In order to pass it, we simply use data property of the state object, just like so:
We should add this setup to every state that invokes our child machine.
Now let’s go back one more time to our child machine config.
Whenever we invoke the machine, we are in this first state called fetch. In there, we can make another BE call just to check if we are up to date in terms of user data. For now, we have a timer there, so let’s just replace it will an actual call to the back-end (mock function in our case):
For handling errors, we can use the errors object that is a property of formik variable that we initialized with useFormik hook earlier.
Whenever the given input value is invalid, we want to style the input in a different way and provide the user with error message. In case of the UI Kitten library that we use in this project, we can do it by using caption and status props.
When a given input field has an error, we change the input status to danger and provide an error msg as the caption to be displayed below the input box.
Our finished Input component should look somewhere the same as (code wise):
We are almost finished with this part. One more thing that we want to add, is to block the next and back buttons whenever we are either in the fetch or pending state. It will prevent the user from dispatching events during backend calls etc.
Apart from this, it would also be reasonable to additionaly block the next button when the form is in error mode. When we allow the user to invoke the submit despite the input values being invalid, the formik will handle it, but it would be nice from the UI/UX point of view to just format the styling of the button in a different way.
In order to do so, we need to pass some new props down to our FormWrapper component.
Your task is to repeat this step for every screen. It’s just a matter of copying and pasting some code and changing a few things here and there. Machines are already taken care of so don’t worry about them.
At this point, our app should correctly handle the user update process and look somewhat like this:
There’s one more feature we could add to polish our app.
Upon completing the whole form (meaning that the user is in the UserDataStates.complete state), we could either exit this flow and redirect the user to the completely different part of the app, or just simply give him the chance to e.g. go back and get through each one of the steps one more time. Let’s implement the latter and add a button allowing the user to go back to the previous screens. Our machines are currently setup in such a way, we just need to add a button on our success screen. Let’s do so then:
Now, the user can also go back whenever in success screen.
Summary
That’s it, we have made it and created the mobile app together using XState, Formik, Yup and TypeScript! 🤓
It was quite a long article, I hope that you eventually got through it and it helped you in any way at least a bit.
The final code for the app is available in the repo either on the master branch or xstate-formik branch. Check it out here. Feel free to leave a star ⭐️
Custom color picker animation with React Native Reanimated v2
Color picker
React Native
Mobile development
Reanimated
React native reanimated
It'll guide you through the process of creating a color picker animation using React Native Reanimated v2 hooks: useSharedValue, useAnimatedGestureHandler, useAnimatedStyle.
How to set video as a background in React Native application
React Native Video
Video background
React Native
Explore step-by-step guidance on integrating video backgrounds into your React Native applications using the react-native-video library. This article provides practical tips and code examples to enhance your app's UI with dynamic video elements, suitable for both beginners and experienced developers in React Native.
Have you ever wondered what is the best way to handle errors in Your app? RN Notificated seems to be an answer for Your doubts. Fully customized, fast, and most importantly easy to use!
Oops! Something went wrong while submitting the form.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.