How to create custom forms with validation and scroll to invalid logic in React Native (Part two: Scroll to invalid)
Tags: react, reactnative, mobile, javascript
Published: 2019-07-08 15:02:37

---
title: How to create custom forms with validation and scroll to invalid logic in React Native (Part two: Scroll to invalid)
published: true
description: A tutorial on creating custom forms in React Native. Part two focuses on the scroll to invalid logic.
tags: #react #reactnative #mobile #javascript
cover_image: https://thepracticaldev.s3.amazonaws.com/i/bv8l6czhojgqfik38c3p.png
series: How to create custom forms with validation and scroll to invalid logic in React Native
---

*Want to stay up-to-date? Check out React Native Now, the bi-weekly [React Native newsletter](https://reactnativenow.com/)*

---

In the [first part](https://dev.to/lfkwtz/how-to-create-custom-forms-with-validation-and-scroll-to-invalid-logic-in-react-native-part-one-validation-269o) of this series, we walked through creating a simple form with some helper methods that enabled us to roll our own validation logic. In this part, we'll walk through how we can make our forms automatically scroll up to the first invalid element.

## Locating the elements

The first step needed to accomplish this will be to ensure our local state's input objects are updated to store the Y value where each individual input lives. To store this, we will create a helper called `setInputPosition` that will add a `yCoordinate` key onto each of our inputs.

```js
function setInputPosition({ ids, value }) {
  const { inputs } = this.state;

  const updatedInputs = {
    ...inputs
  };

  ids.forEach(id => {
    updatedInputs[id].yCoordinate = value;
  });

  this.setState({
    inputs: updatedInputs
  });
}
```

This helper will take an array of `ids` that sync up with the input objects living in our local state. The advantage of using an array here is that we could potentially have multiple inputs existing on the same row (like we've done already in our demo app with birthday month and year). Since both of these inputs will be sharing the same `yCoordinate` value, we can call this helper method one time and update both.

Now that we have our helper created, bind it to the constructor like many of the previous helper methods - since it will be interacting with our state.

To use it, we'll need to tap into the [onLayout](https://facebook.github.io/react-native/docs/view#onlayout) method that is exposed on many React Native components. The onLayout method will be invoked on mount and after any layout changes and receive an object that contains details about that elements position **in relation to its parent View** (more on that later).

So, let's test out calling this method on our form's first input - `first_name`.

```js
onLayout={({ nativeEvent }) => {
    this.setInputPosition({
        ids: ["first_name"],
        value: nativeEvent.layout.y
    });
}}
```

Now, when the form is loaded, we can take a look at the local state in our debugger and we should see this:

```js
inputs: {
    first_name: {
        type: 'generic',
        value: '',
        yCoordinate: 17
    }
}
```

Our `yCoordinate` was successfully saved to our state and our form is now aware of the exact position of our input within the ScrollView.

Next, we'll add the helper method onto the `last_name` input and our `birthday_month` / `birthday_day` inputs. For the birthday inputs though, we'll add the helper only once on the outer `View` that contains both of these elements and include both keys in the `ids` array. At this point, our form demo app looks like [this](https://github.com/lawnstarter/react-native-form-helpers/blob/a903f529e8f9153969b1e090a461c336b8d686bd/demoApp/App.js).

If we reload the page and check our debugger again, we'll see our local state:

```js
inputs: {
    first_name: {
        type: 'generic',
        value: '',
        yCoordinate: 17
    },
    last_name: {
        type: 'generic',
        value: '',
        yCoordinate: 17
    },
    birthday_day: {
        type: 'day',
        value: '',
        yCoordinate: 142
    },
    birthday_month: {
        type: 'month',
        value: '',
        yCoordinate: 142
    }
}
```

Wait, something looks off here... our birthday month and days should have the same values, but why do our first and last names share the same value? Shouldn't our `last_name` input have a higher `yCoordinate` value since it's lower on the screen?

If you take a look at [line 75](https://github.com/lawnstarter/react-native-form-helpers/blob/a903f529e8f9153969b1e090a461c336b8d686bd/demoApp/App.js#L75) in our demo app, you'll see the following:

```js
<View style={styles.container}>
  <ScrollView>

    // first_name inputs are here

    <View> // <--- LINE 75 HERE
      <Text>Last Name</Text>
      <TextInput
        style={styles.input}
        onChangeText={value => {
          this.onInputChange({ id: "last_name", value });
        }}
        onLayout={({ nativeEvent }) => {
          this.setInputPosition({
            ids: ["last_name"],
            value: nativeEvent.layout.y
          });
        }}
      />
      {this.renderError("last_name")}
    </View>

    // birthday_month and birthday_year inputs are here

  </ScrollView>
</View>
```

Can you spot the issue? Remember, the `onLayout` method returns the location of the element **in relation to its parent View**. So our `last_name` input is effectively telling us the height of the `Text` element here, instead of the location of this input on our screen. This also means our `first_name` input is making the same mistake.

How can we solve this? One of two ways. We could move the `Text` and `TextInput` out of the wrapping `View` so every element is a direct descendant of our parent `ScrollView`. Or, we can move our `onLayout` logic into the wrapping `View`. Let's do the latter.

Now, when we reload and check our local state we should have a `yCoordinate` of 0 for `first_name` and 71 for `last_name`. That sounds more accurate.

## Determining the first invalid element

All of our form elements currently fit on the screen, so let's add some additional input and spacing so our form actually scrolls a bit.

Feel free to get creative here and practice what we've worked on up to this point - including testing out new types of validation. If you want to skip ahead, you can copy the updates I made [here](https://github.com/lawnstarter/react-native-form-helpers/blob/19f07015491a58a683c54a4bb5fa2fb25aae6d8a/demoApp/App.js) and [here](https://github.com/lawnstarter/react-native-form-helpers/blob/19f07015491a58a683c54a4bb5fa2fb25aae6d8a/demoApp/validation/dictionary.js).

![](https://thepracticaldev.s3.amazonaws.com/i/fft072q99ckr7l482zzw.png)<br>Our form, in its current form.

At this point, we have a long form that's aware of each input's position, properly validates all inputs, and marks the invalid ones for our users to fix. Now we need to determine which invalid element is the first one - meaning the input that is both invalid **and** has the lowest `yCoordinate` value.

To determine this, let's write one more helper.

```js
function getFirstInvalidInput({ inputs }) {
  let firstInvalidCoordinate = Infinity;

  for (const [key, input] of Object.entries(inputs)) {
    if (input.errorLabel && input.yCoordinate < firstInvalidCoordinate) {
      firstInvalidCoordinate = input.yCoordinate;
    }
  }

  if (firstInvalidCoordinate === Infinity) {
    firstInvalidCoordinate = null;
  }

  return firstInvalidCoordinate;
}
```

This method will take our entire input state **after** we've run it through our validation service and iterate through each invalid input, continually replacing the `firstInvalidCoordinate` value with a lower value if one is found.

We'll also want to update our `getFormValidation` method to return the result of this helper by adding the following as the last line:

```js
return getFirstInvalidInput({ inputs: updatedInputs });
```

Now in our `submit` method in our form, if we `console.log` out the result of calling `this.getFormValidation()` we should see the lowest `yCoordinate` value - representing the first invalid element on the screen.

## Scrolling to the first invalid input

All this work so far has been to prepare us for the real purpose of this tutorial, actually automatically scrolling our user's device to the first invalid element. This way, they know what they need to correct, and will be able to see any other invalid inputs as they scroll back down the screen.

To interact with our `ScrollView` programmatically - we'll need to create a reference to the element on our constructor and attach it via the `ref` attribute. More details on that can be found [here](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs).

```js
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.scrollView = React.createRef();
  }
  render() {
    return <ScrollView ref={this.scrollView} />;
  }
}
```

Now that we have a reference to it, we can call the `scrollTo` method **if** our form is invalid with the exact coordinates we want to scroll to. We can also utilize the `animated` flag to make our scrolling look professional.

```js
submit() {
const firstInvalidCoordinate = this.getFormValidation();

    if (firstInvalidCoordinate !== null) {
        this.scrollView.current.scrollTo({
            x: 0,
            y: firstInvalidCoordinate,
            animated: true
        });

        return;
    }

    // if we make it to this point, we can actually submit the form
}
```

Alright, let's see how it looks with everything hooked up:

![](https://thepracticaldev.s3.amazonaws.com/i/4jijv4euoosawf0sobhm.gif)

Awesome! Our form has validation and is automatically scrolling to the first invalid input.

Check out our demo app at [its current state](https://github.com/lawnstarter/react-native-form-helpers/tree/53035b3e0ae6e2a3359760ddd6492e89a081e3d7) if something isn't quite working right on your end.

## Next steps

In the third and final part of this series, we'll go over some ways we can improve the validation experience for our users, attempt an alternate method at obtaining the our input's coordinates, and share some insights we've learned from our experiences building out forms in React Native.