• Labs icon Lab
  • Core Tech
Labs

Guided: React Foundations - Props and State

In this guided lab, you will enhance a help desk ticketing application to capture user input, handle events, and pass data between components using basic React props and state patterns.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 2h 9m
Published
Clock icon Jun 06, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Overview

    Welcome!

    In this guided lab, you will enhance a simple helpdesk application to respond to user input and display tickets. As you perform these tasks, you'll learn how components pass data between each other using props and can respond to user input using state.

    What to Expect

    You should be familiar with how to render a React application using JSX and simple components. This was covered in the first lab, React Fundamentals: JSX and Components.

    The JavaScript code in the lab is written in idiomatic React, which is typically ES2015+ syntax. You should be able to work with arrow functions, ES modules, and const.

    info> Stuck on a task? Check out the solution/ folder in the project directory for full solution files and reference code for each step. Files with numbers (like index.2.jsx) correspond to the task order in the step.

    Getting Started

    To make sure the lab environment is set up and working, you'll start by running the Globomantics Helpdesk demo application. Once set up, you can visit {{localhost:5173}} to view the app in your browser, or in the Web Browser tab in the editor to the right.

    What you will see is a plain HTML and CSS webpage from the index.html file.

    info> Web browser not working? Sometimes while reading through the lab, the dev server will shut down and you'll have to reconnect to the Terminal. Just run npm run dev again to start the server back up. Other times, the web browser may not always reflect the latest code due to the auto-save feature of the lab. Just click the refresh button next to the address bar in the tab to force-refresh the page.

    How is the app hosted?

    You may wonder what "VITE" means and how the app is being hosted on the above URL. This comes from the JavaScript development tool Vite.js.

    Vite is a frontend tool that bundles and builds JavaScript applications, including React. There are other options available depending to build and host React applications depending on what you need, such as Next.js.

    Vite is versatile and supports many other frameworks and libraries, including plain HTML and CSS with very little configuration.

    A Simple Helpdesk App

    The application is a simple helpdesk where you can enter ticket information related to a Globomantics product.

    There are several React components you created previously, including:

    • FormGroup to wrap form fields with a special <div>
    • ProductField for creating a dynamic product drop-down
    • Header for encapsulating the header of the page
    • App for hosting our app

    However, there are some glaring issues you have to address!

    Namely: the support team can't create a new ticket!

    Your boss's Slack DM this morning said it all:

    Boss: If you could make sure the team can submit a ticket and then view the details afterwards, that would be swell. Thanks!

  2. Challenge

    Introducing Props

    Introducing Props

    Surprise! You've been using props throughout previous labs, like key, className, name, id, and children... but how do they work exactly?

    When you pass props to a JSX element or React component, you are really creating a JavaScript object behind the scenes.

    Remember how you manually created elements in React in a previous lab? You used the createElement API, which the JSX compiles to, like this:

    React.createElement("form", null, null);
    

    You already learned the third argument is children. The second argument to the function is props and it accepts a JS object.

    So for example, the JSX in the Header component:

    <a href="/">Globomantics Helpdesk</a>
    

    Uses the href prop, and gets compiled to:

    React.createElement("a", { href: "/" }, "Globomantics Helpdesk");
    

    What props can you pass?

    Technically, you can pass any prop to a component or JSX element. The catch is that the element needs to do something with the prop. Again, because it's a JS object behind-the-scenes, an object can contain any amount of properties but if they aren't consumed, they won't do anything.

    JSX HTML elements like main, div, and a all accept standard JavaScript DOM attributes as props. You've used some already:

    • className
    • href
    • name
    • htmlFor

    React handles these on your behalf, creating the underlying DOM elements during rendering and passing these props, like this:

    const anchorEl = document.createElement("a");
    anchorEl.href = "/";
    anchorEl.className = "header-link";
    

    Since props are translated to JavaScript object properties, they can be any valid JavaScript type:

    • Strings
    • Numbers
    • Booleans
    • Arrays
    • Objects
    • Functions

    String props can be passed using quotes, like in the Header component:

    <a href="/">Globomantics Helpdesk</a>
    

    But other non-string types should be passed using JSX expression syntax, like you did in ProductField:

    <option key={name}>{name}</option>
    

    name is a string but it's a variable, so you need to substitute its value using the JSX expression syntax.

    What about the `key` prop?

    The key prop is not a built-in JavaScript DOM property, so how does that work? React layers in some special props on top of JSX like key and ref that it uses internally.

    As you work through this lab, you'll get more familiar with passing props around.

    Submitting the form

    In a traditional server-side application, the ticket <form> would POST to the server at the URL specified in its action attribute. The server would then take the ticket data and render a new version of the page (or redirect to a new page) to view the details.

    In a client-side React application, you need to intercept this form submission and handle it in the browser. You can do that using the onSubmit prop for the <form> JSX element.

    Props as event handlers

    Props that accept functions are usually called "event handlers" (or "handlers" for short). The onSubmit prop is a handler and accepts a JavaScript function callback that is passed a JavaScript DOM SubmitEvent object.

    Handling form submission

    The first step to creating a ticket is to implement a simple JavaScript form submission handler so you can access the ticket data. The <form> JSX element you just wrote compiles to:

    React.createElement("form", {
      onSubmit: handleFormSubmit
      action: "/", 
      method: "post" 
    }, /* ...children */);
    

    This should make it a little clearer why JSX accepts JavaScript expressions in its syntax -- it all gets compiled to JS function calls!

    How does `onSubmit` get handled?

    You may notice that there is no onSubmit property on the HTMLFormElement object in native JavaScript, it's actually onsubmit.

    React wraps these native DOM events with event handler props prefixed with on and then the event name, like:

    • onChange
    • onSubmit
    • onClick

    React does this for standardization and for internally taking care of any browser quirks so you don't have to.

    In the console logs in the Web Browser, when you submit the form, you can see the event logged to the console is the type BaseSyntheticEvent. They are called "synthetic events" because they are events React emits that wrap the native browser events.

    ## Preventing form submission

    Go ahead and submit a ticket in the Web Browser tab. Notice that it still results in a blank page.

    Just because you added a JavaScript event handler doesn't mean you prevent the event from bubbling up to the browser.

    To fix this, you can call the Event.preventDefault() API to prevent the event from bubbling up.

    In this case, you want to prevent the form from issuing a POST request which doesn't work in this app since you don't have a backend server. OK, you've successfully prevented the form from submitting to the server. Is there a way to see what values were submitted?

    Extracting form values

    Within the body of the handler, you can extract form values using the FormData web API and passing in the form's DOM element reference to the constructor, like this:

    const form = e.target;
    const formData = new FormData(form);
    

    You can then use the FormData.get(name) method to retrieve the form values by input name.

    To be clear, this is not a React API you need to import -- this is a native browser web API. Within the event handler, you are not within the scope of React anymore. React concerns itself mainly with rendering and handling events, but after that, you are writing plain old JavaScript. Now after you fill out the ticket form, and click the Create ticket button, in the developer console you'll see something like this printed out:

    { summary: "...", body: "...", product: "..." }
    

    The form submission event is being handled and you assembled a ticketData object containing the form values.

    Now, what can you guess what you'll do next with the ticket data? Display it!

  3. Challenge

    Passing Data Between Components

    Passing Data Between Components

    Now that you've captured ticket data into an object, you can display the submitted ticket information in the UI.

    To do that, you need to somehow pass the ticketData object to other components in your app so you can render the values. How can you do that from your form event handler?

    The short answer is: you can't.

    The function you declared is outside the scope of the <App> component, so there are no React APIs you can utilize to help you store the data and pass it around.

    info> Remember: React components encapsulate behavior and data. By declaring the handler outside a component, you are not really following the principles of React. If you moved the App component to another file, the handler would not come with it. It isn't "co-located."

    The good news is that with a minor refactoring, you can take advantage of React props to pass data around the component tree.

    Creating a TicketForm component

    In order to better co-locate the form submission handler with the form rendering, you should extract a TicketForm component from the App so you can move the handleFormSubmit callback from a module scope into a component scope.

    Why not move the function to `<App>`?
    Technically, you could. However, it is usually better to keep behavior and logic related to a subset of the application together. By extracting a `TicketForm` component, it's easier to understand that the form submission handling only relates to the `<form>` and not the entire application. You are effectively "hiding" the form submission details from the rest of the app -- that is the essence of encapsulation.
    
    With the `TicketForm` extracted, you now have a few more options available to pass the `ticketData` around the application.

    To keep things simple, you'll display the new ticket information alongside the submission form.

    Creating a TicketDisplay component

    To begin with, create a new TicketDisplay component that will display ticket data that is passed through props. This will be rendered within the <App> component. There is now a section of the page that should display submitted ticket data.

    However, if you try submitting ticket data again in the browser... nothing happens. This makes sense, as you haven't yet done anything with the ticketData object in the form submission handler.

    The problem is: how can you pass data up to the <App> component so that you can then pass it back down to the <TicketDisplay> component?

    Passing data up through prop callbacks

    Prop callbacks to the rescue! Just as you handled the form submission event, you can declare your own handler prop in the TicketForm component. This way, the <App> component can pass a callback that will receive the ticketData object.

    In React, it's a convention to prefix handler props with on to denote they are callbacks for event handling. You can declare a component that accepts a callback prop simply through destructuring it or expecting it to be passed:

    const HelloWorld = ({ onGreeting }) => {
      return (
        <button onClick={() => onGreeting('Hello World!')}>
          Say Hello
        </button>
      )
    };
    

    In the example above, notice how you can call the prop callback and pass it data from within the scope of the component. This is how you can pass data back up the component tree.

    What's a callback, again?
    A callback is a function that is passed around as a value so that _another_ piece of code can call it and pass some data to it. In JavaScript, functions are first-class citizens, they are like tiny packages you can ship around to different parts of your codebase just by passing them by reference.
    

    For example, the typical event handler is a callback that accepts an event object:

    const handleFormSubmit = (event) => {
      // do something with `event`
    };
    

    It can be passed around by reference to React components via props or even to other functions.

    Now the `App` component is handling the `onTicketCreated` callback which is provided the `ticketData` as an argument.

    This successfully passes the data up the tree to App.

    The next question is: how can you pass it back down to the TicketDisplay component through its props? It is currently "stuck" inside the callback handler and can't get out!

  4. Challenge

    Introducing Component State

    Introducing Component State

    In order to "capture" the ticketData object to pass in the JSX, you need some way of introducing local state to the component.

    Normally in JavaScript applications, that's what variables are used for.

    Storing local component state

    Why don't you try introducing a variable in the component, and assigning it a value when the callback is executed. Will that work? Now if you run the app in the browser, you'll see a blank page! If you check the console, you would see an error:

    Cannot read properties of undefined (reading 'summary')

    Oops! This is because the ticket variable is not initialized with a value so by default it is undefined.

    In React, one common pattern you'll use is to conditionally render a part of the component tree if an expression is falsy.

    Conditional rendering

    In this case, you want to avoid rendering the TicketDisplay component if the ticket data isn't populated. This is called "conditional rendering."

    Within the JSX, you can use a ternary conditional expression like this:

    <div>
      {myVariable 
        ? <MyComponent name={myVariable.name} /> 
        : null}
    </div>
    

    Notice how the condition will return null if myVariable is falsy, otherwise it renders the component. Since this is "just" JavaScript, myVariable.name will not be evaluated unless myVariable is truthy, avoiding the error.

    What about the `&&` conditional?

    One approach to conditional rendering you may see in the wild is like this:

    <div>{myVariable && 
      <MyComponent name={myVariable.name} />}</div>
    

    This avoids some mental gymnastics of ternary expressions but has a subtle bug. If myVariable is false or 0 sometimes it can be treated as a string and rendered into the page!

    A ternary expression makes sure that never happens by forcing a null (or empty) render if the expression returns false.

    Now if you open the browser, the app is rendering. Try submitting a ticket now!

    Did the ticket data get displayed? No? Don't worry, it's not your fault. This is where plain JavaScript doesn't quite work the way you initially expect with React components.

    Hooking into React

    Using local variables like this is not recommended for React apps because React has no idea your local variables exist. In other words, React will never know when the value of your variable is updated so it can't re-render the changes.

    Instead, you need a way to declare local variables that are "hooked" into React's engine and notify it of changes.

    Storing component state

    React offers the useState API to do exactly that. It allows you to store a value and when it gets updated, internally it "notifies" React and then React re-renders the component with the updated value. This is called "component state" and its really what makes React, well, reactive.

    The useState function accepts a default value (if any) and returns an array with two objects, the current value and a callback function (called the "set-state action").

    The idiomatic way to write a useState statement is using array destructuring syntax:

    const [currentValue, setValue] = useState(false);
    
    Syntax: What is array destructuring?

    Array destructuring syntax was introduced in ES2015 to make it simpler to extract items from an array and assign them to variables.

    For example, the code above is equivalent to this:

    const myState = useState(false);
    const currentValue = myState[0];
    const setValue = myState[1];
    

    Array destructuring makes this simpler and combines all these statements into a single statement.

    Updating component state

    When you want to update the current value of a state variable, you call the set-state action (or callback) with the new value.

    The set-state action accepts a single argument for the value to set, like this:

    setValue(true);
    
    Passing a set-state callback You can optionally pass a callback that gets passed the previous state values, for example to toggle the value:
    setValue(prevState => !prevState);
    

    This can be useful in situations where you need to reference previous state because set-state actions are executed asynchronously, and there's no guarantee the value will be correct if you reference the state variable directly due to JavaScript closures.

    To tell React to re-render with the new ticket data, you'll need to refactor the the way you set the ticket data variable to use the useState function instead. Now in the Web Browser if you try to fill out the form and submit a ticket, the TicketDisplay component renders with the submitted data!

    Go ahead and modify the ticket data and re-submit the form. Each time, the displayed data is updated.

    Congratulations, you satisfied your boss. Right?

  5. Challenge

    Passing External State

    Passing External State

    As you show the new Helpdesk application to your boss, they tell you they're happy with what you've built but it would be great if they could create links to pass the product name in the URL to pre-populate the form.

    For example, like so:

    {{localhost:5173}}/?product=GloboCMS

    Boss: This would make it easier when handling support tickets for customers, so that the team doesn't need to manually fill in the product field every time. Please tell me you can do that, right?

    Indeed you can! You can pass data from outside React into your components since they are all "just" JavaScript functions and you can execute any logic you normally would on a web page.

    URLs are stateful

    The URL is an example of external state -- state that is stored outside of React. A URL can contain a query string (?key=value), route information (/products/1234), or even a hash value (#reviews).

    In order to set the default product in the drop-down, you'll need a way to capture a product name from the query string and pass it to the <ProductField> component.

    The browser stores the current URL details in the window.location object. The query string is specifically stored in window.location.search, and will contain the full query string value ("?key=value").

    To parse the query string, you don't need to write any custom code! You can use the native URLSearchParams class and the get(key) method, like this:

    const searchParams = new URLSearchParams(
      window.location.search
    );
    
    // Get a querystring value by key
    const myValue = searchParams.get("myKey");
    

    Now you can start by reading the query string from the URL in the App component so that its loaded when the application renders initially. Now that you are reading the value of product from the URL query string, you need to pass it all the way down to the product drop-down, which is within the ProductField component.

    You'll need to go down each layer of the component tree and pass the value using props.

    Drilling props

    To pass data down from a top-level component down to a lower-level component, you need to use a technique called "prop drilling."

    Prop drilling is an easy way to pass data from a top-level component down deeper into the component tree. Now that the <select> drop-down supports a default value, click the link below.

    {{localhost:5173}}/?product=GloboHR

    The form will now render with the product field set to "GloboHR" from the URL. If you try changing the product, then submit the form, it even resets to the default value again.

    Why doesn't GloboFinance+ work?

    The + character is a special character in a URL, so you need to encode it as %2B. This is called percent-encoding.

    Your boss is ecstatic now -- thank you for adding that feature! You know what would be even better though? If, after submission, the form remembered the last product you chose instead of resetting it to the default value.

    Boss: Our customer support folks will be entering a lot of tickets one after the other, so having to change the product after submission will get a bit tedious.

    Alright, how can you do this? The data in the form is reset on submission using the native web API, HTMLFormElement.reset.

    Surely you can't control or override that... can you?

  6. Challenge

    Controlling Form Inputs

    Controlling Form Inputs

    Surprise (again)! In this application, you have been using a pattern in React called "uncontrolled inputs." This means, you rely on the browser to manage the state of the form field values. As the user types, the browser is tracking the value, and then you can get the final form values on submission in the handleFormSubmitted event handler using FormData.

    The problem as you've seen is that when form.reset() is called, which is a native browser function, that the value of the product input is reset to its default selection (GloboCMS).

    This approach can work well for simple forms, however your new requirement is to "remember" the last value after submission.

    This is called creating a "controlled input" and it's when you tell React to take over and opt-out of browser handling.

    Controlling the product value

    To create a controlled input, you have to set its value prop. In order to do that, you'll start by introducing local component state to the <ProductField> component to hold the value of the selected product. Fantastic, now try changing the product drop-down after the page loads.

    Is it working? No? What's going on? It seems "stuck" and won't change the value!

    Handling input changes

    As you've taken over control from the browser, you are passing the form's value using local state. However, you aren't yet updating the state using the set-state action, are you?

    This is working "as designed." You now have full control over the value of the input. This is why sometimes creating controlled inputs adds more overhead, so it's always a good question to ask: do you really need to use controlled inputs for all form fields?

    In order to update the selected product, you need to pass a callback to the onChange handler prop for the <select> element, then call the set-state action. OK, now try changing the product in the form. It's working! What's more, when the form is submitted, the state value is retained and the product field doesn't change to the defaultValue anymore.

    Your boss should be pretty happy, but there's something you as the developer need to be aware of.

    Avoiding malicious input

    Try clicking this link to set a product that doesn't exist:

    {{localhost:5173}}/?product=NotARealProduct

    The product drop-down doesn't show this fake product, but let's try logging the state value in the ProductField component. When you click the malicious link, In the browser console you should see:

    "NotARealProduct"

    Uh-oh! As a malicious user, you've injected your own fake product into the local component state.

    Default state values

    When using defaultValue and an uncontrolled input originally, the browser ensures the value of the <select> matches one of the <option> values. However, now that you are storing your own local state, it's up to you to validate the selected option!

    info> A best practice is to never trust user input and always sanitize or validate it on the client-side even if your server or backend has validation.

    Before you send this to your boss, you should take a security-first approach and validate the default product against your products array (the "source of truth"). That way, no one can enter a value that doesn't exist in the list. Now try clicking the malicious link again:

    {{localhost:5173}}/?product=NotARealProduct

    If you log the value of the state variable to the console, you would see it is now undefined instead of the fake product.

    info> Note: Instead of undefined you could choose a different product to be the default value. The browser will handle an undefined value natively, using the first option in the list as the default value.

    A security-first mindset should be taken across all aspects of the application, from the database to the client-side. This is commonly referred to as "defense-in-depth." Remember, security is only as strong as the weakest link!

    When you use controlled inputs in React, you need to be thinking about how to handle bad or malicious data from users.

  7. Challenge

    Recap and What's Next

    Recap

    Congratulations, you now have an interactive Helpdesk application that displays newly created tickets for the support team!

    Here's what you just learned:

    • You handled events using callback functions passed as props
    • You passed data up through the component tree using props
    • You passed data down through the component tree using "prop drilling"
    • You used the useState API to introduce local component state to notify React to re-render the application when new ticket data is submitted
    • You captured external state from the URL to use within your app
    • You took a security-first approach by validating the product passed from the URL as user input
    • You learned how to perform conditional rendering
    • You used "uncontrolled" form inputs to let the browser handle and set input values
    • You added a "controlled" form input for the product field to opt-in to using React component state for the input value

    That's a lot. You deserve a pat on the back!

    What's Next

    While displaying a single submitted ticket is somewhat helpful, there are a few things that would be ideal in an app like this:

    1. The support team needs to manage tickets after creation. Can you display a list of submitted tickets?
    2. The user provided ticket data, but the support team needs additional data like "time created" or even a ticket identifier.
    3. The data disappears when reloading the page. That won't be feasible for a production deployment!

    To handle these cases, you'll have to leverage more APIs like useState, such as useCallback, useContext, and useEffect -- these are called hooks and they are fundamental to building React apps. In addition, you'll need to work with asynchronous code to fetch data from a database or API so you can update application state.

    In the next lab, React Fundamentals: Hooks and Context, you'll explore even more APIs by enhancing the Helpdesk app to store data in a database.

Kamran Ayub is a technologist specializing in full-stack web solutions. He's a huge fan of open source and of sharing what he knows.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.