As of React v16.8, function-based components have a lot more capability which includes the ability to manage state. In this post we are going to go through how we can use the Reacts useState function to manage state within a strongly-typed functional component with TypeScript. We are going to build a sign up form like the one below:

Setting up our project

Firstly, let’s create a React and TypeScript app with create-react-app as detailed in the last post.

At the time of writing this post, v16.7 is still in alpha, so, let’s update our app to use the alpha version of React:

npm i react@next react-dom@next

We’re going to shrink the size of the app header so that we can see our sign up form. Let’s make the following changes in App.css :

.App-logo { height : 40px ; } .App-header { min-height : 150px ; font-size : 16px ; margin-bottom : 30px ; }

Whilst we are in App.css , let’s add some styles for our form so that looks okay:

form { width : 220px ; margin : 0px auto 0px auto ; text-align : left ; display : flex ; flex-direction : column ; font-size : 16px ; } form .row { margin-bottom : 15px ; } form input { width : 100% ; font-size : 16px ; margin : 3px 0px 3px 0px ; padding : 3px 5px ; color : #464444 ; } form .error { color : red ; font-size : 12px ; } form button { font-size : 16px ; color : white ; background-color : #484a4e ; border : #484a4e solid 1px ; border-radius : 3px ; padding : 5px 12px ; cursor : pointer ; } form button:hover { background-color : #282c34 ; border-color : #282c34 ; } form button:disabled { background-color : gray ; border-color : gray ; cursor : not-allowed ; } form .submit-success { color : green ; } form .submit-failure { color : red ; }

Creating a basic sign up form

Now that our project is setup nicely, let’s create a component that will contain our sign up form. So, let’s create a file called SignUp.tsx with the following import statements:

import React , { FC , ChangeEvent , FormEvent , useState } from "react" ;

The useState function is the new React function we are going to use a little later to manage our state.

Let’s move on to creating our props type:

export interface ISignUpData { firstName : string ; emailAddress : string ; } export interface ISignUpResult { success : boolean ; message : string ; } interface IProps { onSignUp : ( data : ISignUpData ) => ISignUpResult ; }

So, the consumer of the component will interact with the web server and do the sign up submission. The consumer will need to handle an onSignUp function prop that has a parameter containing the first name and email address and needs to return whether the sign up was successful and if not return the error message.

We can create the shell of our function component now:

export const SignUp : FC < IProps > = props => { return ( < form noValidate = { true } > < div className = " row " > < label htmlFor = " firstName " > First name </ label > < input type = " text " id = " firstName " /> < span className = " error " > </ span > </ div > < div className = " row " > < label htmlFor = " emailAddress " > Email address </ label > < input type = " text " id = " emailAddress " /> < span className = " error " > </ span > </ div > < div className = " row " > < button type = " submit " > Sign Up </ button > </ div > < div className = " row " > < span > </ span > </ div > </ form > ) ; } ;

Let’s start to make the first name and email address controlled inputs:

< div className = " row " > < label htmlFor = " firstName " > First name </ label > < input id = " firstName " onChange = { handleFirstNameChange } /> < span className = " error " > </ span > </ div > < div className = " row " > < label htmlFor = " emailAddress " > Email address </ label > < input id = " emailAddress " onChange = { handleEmailAddressChange } /> < span className = " error " > </ span > </ div >

We have referenced change event handlers, so, let’s create them just before the return statement:

const handleFirstNameChange = ( e : ChangeEvent < HTMLInputElement > ) => { } ; const handleEmailAddressChange = (e: ChangeEvent < HTMLInputElement > ) => { } ; return ( ... )

Managing state

Now we have reached the interesting bit. How are we going to store and set the state for the first name and email address? Well, we can leverage that useState function we imported earlier on:

const [ firstName , setFirstName ] = useState ( "" ) ; const [ emailAddress , setEmailAddress ] = useState ( "" ) ; const handleFirstNameChange = ... const handleEmailAddressChange = ...

The useState function takes in the default value of the state and returns a two element array containing the state variable and a function to set the value of the variable.

So, we have destructed the returned array to create two state variables, firstName and emailAddress as well as two functions to set their values called setFirstName and setEmailAddress .

We can now reference these variables in our JSX:

< div className = " row " > < label htmlFor = " firstName " > First name </ label > < input id = " firstName " value = { firstName } onChange = { handleFirstNameChange } /> < span className = " error " > </ span > </ div > < div className = " row " > < label htmlFor = " emailAddress " > Email address </ label > < input id = " emailAddress " value = { emailAddress } onChange = { handleEmailAddressChange } /> < span className = " error " > </ span > </ div >

We can carry on implementing our change handlers now:

const handleFirstNameChange = ( e : ChangeEvent < HTMLInputElement > ) => { setFirstName ( e . currentTarget . value ) ; validateFirstName ( e . currentTarget . value ) ; } ; const handleEmailAddressChange = (e: ChangeEvent < HTMLInputElement > ) => { setEmailAddress ( e . currentTarget . value ) ; validateEmailAddress ( e . currentTarget . value ) ; } ;

Let’s create the validator functions we just referenced:

const validateFirstName = ( value : string ) : string => { const error = value ? "" : "You must enter your first name" ; return error ; } ; const validateEmailAddress = ( value : string ) : string => { const error = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ . test ( value ) ? "" : "You must enter a valid email address" ; return error ; } ;

We need some more state for the validation error messages. So, let’s create these state variables:

const [ firstName , setFirstName ] = useState ( "" ) ; const [ firstNameError , setFirstNameError ] = useState ( "" ) ; const [ emailAddress , setEmailAddress ] = useState ( "" ) ; const [ emailAddressError , setEmailAddressError ] = useState ( "" ) ;

Let’s finish implementing the validator functions:

const handleFirstNameChange = ... const validateFirstName = ( value : string ) : string => { const error = value ? "" : "You must enter your first name" ; setFirstNameError ( error ) ; return error ; } ; const handleEmailAddressChange = ... const validateEmailAddress = ( value : string ) : string => { const error = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ . test ( value ) ? "" : "You must enter a valid email address" ; setEmailAddressError ( error ) ; return error ; } ;

We can also render the validation errors by referencing the error state variables in the JSX:

< div className = " row " > < label htmlFor = " firstName " > First name </ label > < input id = " firstName " value = { firstName } onChange = { handleFirstNameChange } /> < span className = " error " > { firstNameError } </ span > </ div > < div className = " row " > < label htmlFor = " emailAddress " > Email address </ label > < input id = " emailAddress " value = { emailAddress } onChange = { handleEmailAddressChange } /> < span className = " error " > { emailAddressError } </ span > </ div >

Let’s focus on the submission of the form now and create a handler for it:

const handleSubmit = ( e : FormEvent < HTMLFormElement > ) => { e . preventDefault ( ) ; const firstNameValidationError = validateFirstName ( firstName ) ; const emailAddressValidationError = validateEmailAddress ( emailAddress ) ; if ( firstNameValidationError === "" && emailAddressValidationError === "" ) { const result = props . onSignUp ( { emailAddress , firstName } ) ; } } ; return ( < form noValidate = { true } onSubmit = { handleSubmit } > ... </ form > );

So, we double check that the first name and email address are okay before invoking the onSignUp function prop.

We want to inform the user whether the submission was successful or not, so, we need some more state for this. Firstly, we need state to indicate the form has been submitted:

const [ submitted , setSubmitted ] = useState ( false ) ;

Secondly, we need state for the submission result:

const [ submitResult , setSubmitResult ] : [ ISignUpResult , ( result : ISignUpResult ) => void ] = useState ( { success : false , message : "" } ) ;

Notice how we ensure the destructured state variable and setter function are strongly-typed.

We can now finish off our submit handler:

const handleSubmit = ( e : FormEvent < HTMLFormElement > ) => { e . preventDefault ( ) ; const firstNameError = validateFirstName ( firstName ) ; const emailAddressError = validateEmailAddress ( emailAddress ) ; if ( firstNameError === "" && emailAddressError === "" ) { const result = props . onSignUp ( { firstName , emailAddress } ) ; setSubmitResult ( result ) ; setSubmitted ( true ) ; } } ;

Let’s show the submission result if the form has been submitted. Let’s also disable the submit button if the form has been successfully submitted:

< div className = " row " > < button type = " submit " disabled = { submitted && submitResult . success } > Sign Up </ button > </ div > ; { submitted && ( < div className = " row " > < span className = { submitResult . success ? "submit-success" : "submit-failure" } > { submitResult . message } </ span > </ div > ) ; }

Consuming SignUp

Our final job is to reference our SignUp component in our App component:

... import { SignUp , ISignUpData } from "./SignUp" ; class App extends Component { render ( ) { return ( < div className = " App " > < header className = " App-header " > ... </ header > < SignUp onSignUp = { this . handleOnSignUp } /> </ div > ) ; } private handleOnSignUp ( data : ISignUpData ) { console . info ( "SignUpData" , data ) ; return { message : "Congratulations, you have successfully signed up!" , success : true } ; } }

Let’s start our app in our development server and give this try:

npm start

If we hit the Sign Up button without filling in the form, we correctly get the validation errors rendered:

If we properly fill out the form and hit the Sign Up button, we get confirmation that the form has been submitted okay:

Wrap up

A huge benefit of using a function-based component for our sign up form over a class-based component is that we don’t have to manage the “this” problem - in fact we don’t have to reference this at all.

There is another approach we can take to manage state in function-based components which is perhaps beneficial when the state management is more complex. We’ll refactor our sign up form in the next post and use this alternative method.

The code in this post is available at https://github.com/carlrip/ReactFunctionComponentState/tree/master/useState

Comments

Max December 4, 2018

I thought function-based components were supposed to be stateless more or less by definition. Isn’t this muddying the water?

Carl December 5, 2018

Thanks for the question Max. Yes, before React 16.8 we could only have stateless function-based components. React 16.8 will allow function-based components to do things like manage state and implement logic in lifecycle hooks. This will allow us to use function-based components for the majority of our components if we want rather than having a mix of function-based and class-based components.

Regards, Carl