Make Dynamic Forms with React

Kent C. Dodds
InstructorKent C. Dodds

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 2 years ago

Static forms don't give the user a lot of information about how to submit the form. We can use Reacts onChange prop on an input to dynamically update the form; disabling and enabling the submit button on a condition. This allows for custom validation as the user makes changes to the input.

Instructor: [00:00] Here, we have a name form. If I say hi in the name form, I'm going to get an error that the value must be at least three characters, but it's only two. Then I say hey, and then it says it must include S. OK, fine, I'll do heys, and now I have success.

[00:14] This kind of experience is not really super awesome, because I have to keep on trying and hitting submit. What would be much better is if the submit button were disabled or something, not even rendered, and I get an error message as I'm typing.

[00:27] That's the kind of experience we're going to build. The way that this works right now is we have this form that we're rendering, and on submit, we go ahead and prevent the default behavior for the form. We get the value from the username element, and then we get the error from our getErrorMessage prop.

[00:45] If there is an error, then we alert with the error, otherwise we alert with success. Our getErrorMessage prop here is a function that accepts a value and validates it. If we want to validate this thing in real time, then we're going to need to keep some state around that tells us whether or not this is valid.

[01:00] I'm going to go ahead and add a state property here that has an error state. We'll just start that as null. Then on our input, we'll add an onChange. Here, we'll say this.handleChange. Then we'll create a public class field here called handleChange, and this handleChange is going to accept our event.

[01:22] We can get the target. We'll get the value out of the event.target, and then we'll call this.setState with error being this.props.getErrorMessage with the value. Then inside of our render method, we can get the error out of the state.

[01:43] Here, we can say the button is disabled if the error exists. We'll cast that to a Boolean. I say H-I, and I see that it's disabled. Then an S, and it's enabled. If I refresh, this is actually going to initialize as enabled, because we haven't actually done the error message check yet.

[02:01] Let's go ahead and do that when we mount. We'll say componentDidMount, and we basically want to do this same behavior here. We'll just put that right there. Then we don't actually have a value here yet, so we'll just get rid of that, and replace it with an empty string.

[02:14] Now it's initialized with a disabled button, and then we can type his, and it gets enabled. Perfect. Now, let's go ahead and render an error message if the error does exist. We'll say if there's an error, then we'll render a div with the error, otherwise we'll render nothing. Just for fun, we'll add a style with the color of red.

[02:42] Now, we have that error message being shown up right now. You can say hiy, and we get that new error message. Then the error message is gone. In review, to make this work, we didn't actually have to control the value of the input. We just needed to make sure we knew when that input was changing, and we handle that change.

[03:00] When that happens, we say, "Hey, get me the error message," and that sets our error state, which will cause a rerender. If that error does exist, then we're going to render the error message, and we'll also disable the button.

[03:13] We also initialize our state in componentDidMount. Instead of setting state in componentDidMount, we could also just move this directly into the initialization of our state, because at the time that this runs, this.props will already exist, and we'll be able to get that error message.

[03:28] Let's go ahead and do that instead, and everything will work exactly as it had before. In addition, we've also avoided an unnecessary rerender, because we're not actually setting state, we're just initializing the state properly.

Girma Nigusse
Girma Nigusse
~ 5 years ago

after we add <button disabled={Boolean(error)} type="submit">Submit</button>, the alert(error: ${error}) statement inside handleSubmit will never execute. And can be removed.

Arden de
Arden de
~ 4 years ago

after we add <button disabled={Boolean(error)} type="submit">Submit</button>, the alert(error: ${error}) statement inside handleSubmit will never execute. And can be removed.

Agreed. I think this could be slightly confusing to people and might best be removed after adding the boolean

Konekoya
Konekoya
~ 4 years ago

Hi Kent,

Why you return null from getErrorMessage() if the given param doesn't match any of above if statements? And why you use disabled=Boolean(error) instead of disabled={error}

Thanks :)

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Hi Konekoya! I prefer explicitness. It helps future code readers know my intentions. If I didn't return anything, then it could be interpreted that the original coder hadn't considered that case. I also like to be explicit about my booleans, so rather than relying on truthiness/falsiness, I generally cast things to booleans with the Boolean constructor.

I hope that's helpful.

Bijoy Thomas
Bijoy Thomas
~ 4 years ago

Thoughts on returning an empty div element when there is no error?

error ? <div style={{color: 'red'}}>{error}</div> : <div/>

That way the Submit button doesn't jump up.

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Bijoy, that'd be fine. You could also accomplish that with styling. But it really doesn't matter either way.

Brendan Whiting
Brendan Whiting
~ 4 years ago

Isn't it bad UX to yell at people for having invalid input before they've even had a chance to type something? In the angular world, there's these values for a 'dirty' or 'pristine' form that we can use, is there a React equivalent?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

You're correct Brendan, I would probably want to do more work here for a better user experience. React doesn't have support for things like dirty or pristine. You'll have to implement that yourself. You might consider using formik or react-final-form.

Brendan Whiting
Brendan Whiting
~ 4 years ago

Okay cool. I guess this is one of those things where React is more of an 'ecosystem' of 3rd party libraries, for better or worse, and Angular is more of an opinionated framework.

Greg Fisher
Greg Fisher
~ 4 years ago
<NameForm 
    getErrorMessage= {value => {
      if (value.length < 3) {
        ...

Where does the value come from here?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

@Greg, That's provided by the caller (see references to this.props.getErrorMessage).

Greg Fisher
Greg Fisher
~ 4 years ago

Ah, now I see it. Thank you!

Veekas Shrivastava
Veekas Shrivastava
~ 4 years ago

Hi Kent, why did you make getErrorMessage a prop instead of a method inside <NameForm />? Personal preference or is there an advantage to initializing this way?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

It was just a way I could show the functionality without including the implementation details in the NameForm.

Sawyer McLane
Sawyer McLane
~ 4 years ago

Is there any advantage in using

        {error ? (
          <div style={{color: 'red'}}>
            {error}
          </div>
        ) : null}

over

        {error && (
          <div style={{color: 'red'}}>
            {error}
          </div>
        )}
Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

As I said on twitter recently:

I make it a rule to prefer ternaries over &&

I've been burned by react rendering 0 instead of not rendering something because I did:

{users.length && users.map(/* stuff */)}

So I just avoid the problem altogether by using ternaries.

Sawyer McLane
Sawyer McLane
~ 4 years ago

I've been burned by react rendering 0 instead of not rendering something because I did:

{users.length && users.map(/* stuff */)}

Why would this render 0? Is that a bug? Or is there something about map() that I don't understand?

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Try this in your console:

var users = []
console.log(users.length && users.map(() => {}))

You'll get the output is 0

In React, doing the above is effectively doing this:

<div>{0}</div>

It will render 0. This is because 0 is a falsey value so the right side of the && is not evaluated and the left side (0) is used for the expression value.

jdukelee
jdukelee
~ 4 years ago

Hey Kent, When we initialize the components state and call getErrorMessage() with an empty string, the length of value is 0 and the first if condition (value.length < 3) seems to be met resulting in an error message being displayed on first render. I see one way this could be handled is to add another condition to getErrorMessage() i.e. length === 0 or some other 'initializing condition' and return an empty string. Understanding that this example is for learning purposes and a production form may have a very different implementation I'm just interested to hear your thoughts on this and any other approaches you may have to handle this scenario...Thanks much.

Kent C. Dodds
Kent C. Doddsinstructor
~ 4 years ago

Yup, the use cases vary and you have the flexibility of JavaScript to create the user experience that you need :)

Anil Jeeyani
Anil Jeeyani
~ 3 years ago

I am seeing setState method being used from last couple of lessons. is that the this.setState comes with React.Component? so you have to use class to use this method, any other implementation to use this method? any other methods like this that we should be knowing and useful?

Kent C. Dodds
Kent C. Doddsinstructor
~ 3 years ago

There are other things available, but you don't typically use anything other than setState(), props, and state

Tahsin Yazkan
Tahsin Yazkan
~ 3 years ago