- Lab
- Core Tech

Guided: React Foundations - Hooks and Contexts
In this React Fundamentals - Hooks and Contexts guided lab, you will dive deep into advanced React concepts to build and enhance a helpdesk ticketing application. Through hands-on tasks, you will work with asynchronous operations using the useEffect hook and the IndexedDB Web API, and learn to manage shared state efficiently across components with React's Context API. By creating custom hooks and integrating promise-based logic, this lab offers you practical skills in building dynamic, data-driven applications.

Path Info
Table of Contents
-
Challenge
Overview
Welcome!
In this guided lab, you will enhance a simple helpdesk application to persist data to a database. As you perform these tasks, you'll learn how to work with async code, write custom hooks, and share data through the Context API.
What to Expect
You should be familiar with how to pass data between components using props and state. This was covered in the second lab, React Foundations: Props and State.
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, Promises, ES modules, and
let
/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 (likeindex.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:
TicketForm
to encapsulate the form fields and handling form submissionTicketDisplay
to handle displaying submitted ticket information to the user
Additionally, the file tree has been organized into modules within the
src
folder so that it's easier to focus on the new code you'll write throughout the lab.Where is the data being stored?
You can try entering ticket information and viewing the submitted data but if you reload the page, the data is gone!
This is because React state is stored in-memory by default -- you aren't yet persisting it anywhere. A React application is just JavaScript which means you can send data to a database backend through an API, but in this case you will be persisting it to the browser storage which uses the same patterns and practices.
-
Challenge
Introducing the useEffect Hook
Introducing the useEffect Hook
The database you'll store the data in is called IndexedDB and it's supported by all major browsers through a Web API. It is like a document database which makes it simple to store JavaScript objects (serialized as JSON).
Pop quiz: Where would you put the code to work with the database in a React component? In the function body?
You can but that is not the appropriate spot for code like this. Instead, it's better to treat this as a side effect.
What are side effects?
A side effect is code that changes the state of things outside React. You can think of a side effect as running code that depends on data in your React component.
Running code that results in side effects (like creating or opening a database connection) should be run within a special React hook called
useEffect
.Other side effects can include:
- Sending an API request using
fetch
- Setting the
window.location.href
property - Subscribing to
document
events
Is reading data from the URL a side effect?
In the
App
component, you read theproduct
from the URL querystring. Is this a side-effect?No, not technically, as it doesn't change the state of the page or window.
However, you may prefer to still wrap this code in a
useEffect
since it interfaces with an API outside React (window
) and may depend on props or state of the component.Rule of thumb: if you are writing code against an API outside of React, it probably should be an effect to be safe.
The
useEffect
hook accepts two arguments:- A callback which can safely execute JavaScript code that can contain side effects
- An array of dependencies. If a dependency changes, the effect will be executed again.
You can call a
useEffect
in the body of a functional component, like this:import { useEffect } from "react"; const GreetingComponent = ({ greeting }) => { useEffect(() => { console.log(greeting); }, [greeting]); return <div>{greeting}</div> };
When do you think the
console.log
statement runs? Anytimegreeting
changes, including on the initial mount of theGreetingComponent
.How does React know when to re-run effect callbacks?
The hook dependency array is very important to understand. React treats the values the same way it treats props, by doing a shallow comparison to determine equality across component updates.
If the values are equal to the previous values, React will not run the effect callback again. If any of the values differ, it will run the effect again.
This means there are performance considerations for effects, such as splitting effects based on how often the dependencies change.
Opening a database connection
You'll understand how
useEffect
works better by starting with a simple task: opening a database connection to IndexedDB when the application mounts.The IndexedDB API is asynchronous but it does not use Promises! Instead, it uses event handlers similar to how you handled form submission. For example, to open a database, you can write:
const request = window.indexedDB.open("MyTestDatabase", 1); request.onsuccess = (event) => { // Do something with event.target.result! };
Every IndexedDB operation results a similar request interface with
onsuccess
andonerror
callback events you can handle.Since the hook dependency array dictates how often an effect runs, it means when you only want to run an effect once you should pass an empty dependency array (
[]
). This effectively only runs the effect on mount. When you reload the app, you will see in the browser console the following message:"Successfully connected to IndexedDB", IDBDatabase
The successful result of connecting to IndexedDB is a reference to the database API object. This contains methods and properties to interact with the database.
What if you get an error?
As a browser API, the user can deny permission to store data due to privacy or site settings. This would be the most common case of the request to open a DB failing.
The other reason it could fail is due to insufficient disk space. The
errorCode
will tell you what the problem is, and you will need to handle telling the user what to do.Sharing the database
The goal is to store ticket data in the database on submission, which is currently handled in the
handleTicketCreated
callback in theApp
component.You now have a reference to the database API... but how do you pass it to the other event handler?
If you are thinking "use component state" than congratulations, you are thinking in React. Go ahead and try to submit a ticket in the browser. Afterwards, you should see the following logs:
> "Successfully connected to IndexedDB", IDBDatabase > IDBDatabase
After the initial
console.log
in theonsuccess
handler, there is another log called from thehandleTicketCreated
callback. The value was successfully passed through local state to the other handler!Now that you have a reference to the database API, you can store the created ticket data.
- Sending an API request using
-
Challenge
Creating Custom Hooks
Creating Custom Hooks
To store the
ticket
into IndexedDB, you'll need to use the database API object to do so. However, IndexedDB can be hard to work with if you're unfamiliar and that is not the point of this lab, so to make it simpler, there is a custom hook available for you to work with that uses a Promise-based API.What's a custom hook?
A custom hook is simply a plain JavaScript function whose name starts with
use
and can wrap any React hook or JavaScript code.You may have noticed that the
App
component is becoming more complex, with both auseEffect
hook and auseState
hook to store the database connection. These can both be wrapped up into a custom hook which makes it easier for components to consume.Creating custom APIs
The
useTicketDb
custom hook is defined in thesrc/hooks/useTicketDb.js
module and the hook returns an object representing its API interface with several functions:addTicket(ticket)
-- Stores a ticket object. Promise resolves totrue
if successful.updateTicket(ticket)
-- Updates an existing ticket. Promise resolves totrue
if successful.getTicket(id)
-- Retrieves a ticket by its ID and returns a Promise with the resolved ticket object.getAllTickets()
-- Retrieves all the tickets stored in the database and resolves to an array of ticket objects
You can see one of the major benefits of creating custom hooks is defining custom APIs to interface with other services or external state.
A component now only needs to consume the
useTicketDb
hook and does not need to know anything about IndexedDB to use it.Using a custom hook
You'll notice the functions are referring to a
db
object that doesn't exist -- it's missing the connection request code. This is what you just implemented in the previous step so now you will refactor theApp
component to use this newuseTicketDb
custom hook API. If you try to submit a ticket again in the Web Browser tab and open the browser console, you'll see the same previous messages logged as before.Nothing changed as far as functionality but now the
App
component is cleaner and you now have access to theaddTicket
API to create and store the ticket into the database. If you submit a ticket in the Web Browser tab, after 5 seconds in the browser developer console you'll see... an error?DOMException: Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found.
Uh-oh!
In order to store ticket data, you actually need to define an "object store" in IndexedDB (a collection of objects).
Syncing UI with external state
Notice that even though the
addTicket
function threw an error, the form was still cleared and the new ticket was displayed in the UI.This is because you are still setting local component state using
setCreatedTicket
before awaiting the call toaddTicket
.The problem currently is that the UI is now out-of-sync with the external state of the database.
Advanced: Optimistic updates
You could maintain the current functionality but then roll back the change to local state if it fails to add to the database. This results in a more responsive UX as you are not waiting for the database to confirm a transaction (assuming that in the majority of cases, it will succeed). This is called an "optimistic update" and it's a more advanced way to handle external data requiring much more control over the form state.
-
Challenge
Using Promises in Effects
Using Promises in Effects
In modern JavaScript development, many developers prefer working with Promise-based APIs versus event handlers or Node.js-style callbacks. In order to make working with IndexedDB easier, the idb package wraps the native APIs with Promise-based code.
You can use Promise-based code within
useEffect
hooks but you cannot pass an async function as the effect callback directly like this:/* THIS DOES NOT WORK */ useEffect(async () => { const result = await someAsyncFn(...); /* do something */ }, []);
To work with Promises within
useEffect
callbacks, you have two options:- Call
.then()
on any Promises invoked in the effect callback (this is the more classical approach to working with Promises) - Wrap any async/await code with another
async
function and call that within the hook callback
Even though it feels less modern, to keep things simple you'll use the first method to refactor your code.
The Promise.then method accepts two arguments, a success and error handler:
Promise.then( result => { /* on success */ }, err => { /* on error */ } );
If a Promise is resolved, it invokes the success handler. If it is rejected, it invokes the error handler if it is specified, or raises an exception if not.
Switching to a Promise-based API
Within the
useTicketDb
hook, you will replace the event handler code you originally wrote with theidb
Promise-based API in theuseEffect
callback and create a new object store with theupgrade
option.What does the
upgrade
option do?When you passed the
upgrade
callback option toopenDB
, you ended up specifying a way for IndexedDB to identify objects being stored using anid
property. Furthermore, you set it to auto-increment. This is common in many databases like MySQL, PostgreSQL, or MongoDB -- an ID is auto-generated when a record is inserted and this is something IndexedDB can handle for you too.Now if you try submitting a new ticket, finally, it works!
Kind of.
There's actually a noticeable delay of around 5 seconds between when you clicked the Create ticket button and when the data was displayed.
This introduces a new problem: how do you tell the user something is happening in the background?
Showing user feedback
If a user clicks something and nothing happens, they may think your app is broken. Often for form-based asynchronous code, you may disable the submit button and change its content to indicate the user's data is being processed. This has a second-order benefit of preventing accidental or duplicate data from being submitted.
To accomplish this, you'll need to track the state of the Promise.
Reflecting Promise state
Another pop quiz: how would you reflect the fact that a Promise is being resolved during an effect callback?
You guessed it: local state.
You can introduce a state flag that reflects whether a request is being processed. It can be initialized to
false
and then be set totrue
before waiting for the Promise. When the Promise is resolved or rejected, it can then be reset tofalse
. You could then choose to introduce more state, such as the error message. Now that you're tracking the state of the database creation Promise, how will you reflect it back to the user in the form?The
handleTicketCreated
callback is in theApp
component, but the submit button is within theTicketForm
component.You could pass the
isCreating
state flag down as a prop to theTicketForm
component, and then to the<button>
element (remember, this is called "prop drilling").However, prop drilling tends to add intermediate props that don't have much use and can make prop interfaces confusing. To avoid this, you will take advantage of another React API to share data between components: Contexts.
- Call
-
Challenge
Introducing the Context API
Introducing the Context API
The React Context API allows you to share data throughout the component tree without using props. You can think of it as a
useState
hook that maintains the same value and can be used anywhere underneath its Context.You create a context using the
createContext
API which accepts an initial value (similar to auseState
):import { createContext } from "react"; const TicketContext = createContext({ isCreating: false });
Contexts are made up of two parts: the Provider and the Consumer.
Context providers
A Context
Provider
is a component that takes in the current value and sets the context. The reason you need aProvider
is so that you can write logic and behavior that changes the state of the context within the React component tree. Notice above thatcreateContext
is called once in the module, and is not tied to the React tree.A common pattern is to wrap the
Provider
with a custom component that you export for usage within your React app:import { createContext, useState } from "react"; const TicketContext = createContext({ isCreating: false }); export function TicketProvider({ children }) { const [isCreating, setIsCreating] = useState(false); const contextValue = { isCreating, setIsCreating }; return ( <TicketContext.Provider value={contextValue}> {children} </TicketContext.Provider> ) }
Notice how the
TicketProvider
is a React component that can use hooks. It wraps anychildren
underneath it with the ContextProvider
, passing the current value. By wrapping everything in your<App />
component with the new Context provider, you are making theTicketContext
available to any consumers.Consuming a Context
To consume a context, you can use the
useContext
React hook. It accepts a reference to your Context and then its return value is the value of the Context:import { useContext } from "react"; import { MyContext } from "./MyContext"; function MyComponent() { const contextValue = useContext(MyContext); /* you can now work with the Context */ }
Rather than exporting the Context itself, a common pattern is to wrap the
useContext
with a custom hook exported from the same module (so that your Context is not exposed outside the module scope). TheuseTicketContext
hook can now be used anywhere within your application to access the currentTicketContext
value.This means you can replace the
useState
hook in theApp
component with your new context API as well as consume the context in theTicketForm
component directly to access theisCreating
state. You can now try to submit a ticket again in the Web Browser and you should see the button text switch toCreating...
for 5 seconds after you click it.You feel so proud that you are working with async code and making your app more dynamic that when you reload the page, you forgot you're supposed to show the tickets to the user.
Oh, that's right! Now that the tickets are being stored in IndexedDB, you can update the UI to display all the tickets in the database when the page loads.
-
Challenge
Loading and Displaying Dynamic Data
Loading and Displaying Dynamic Data
You now understand how to track the state of async code and how to update the UI dynamically based on that state to show feedback to the user. You're sharing state using the Context API and have a custom hook to work with the database from any component.
It's time to put everything you've learned to the test now as you add a new section to the UI to display a list of all the tickets in the database, not only when the page loads, but also after creating a ticket. This will create a truly dynamic UX.
Rendering a data list
First, add the new
TicketList
component to your application. The<TicketList />
component renders a list of tickets in a table view. It uses auseEffect
hook to fetch the tickets from the database on mount, as you've seen before. It alsomaps
over the ticket array, rendering<TicketListItem />
components using the ticket ID as akey
and uses "props spreading" to set the component props using ticket data.What is props spreading?
JSX expressions can contain any JavaScript expression, including the object spread operator (
...myVariable
).Since
props
on a React component is an object, spreading props allows you to use the properties of an object to set the props of the component en-masse.<TicketListItem key={ticket.id} {...ticket} />
Props spreading can sometimes have unintended side-effects, like overwriting previously set props or introducing props that cause excessive rendering so it should be used sparingly.
With the
TicketList
added to your app, reloading the page should now display a list of tickets stored in the database!But it doesn't!
Instead it just says:
No tickets created yet.
The reason is because the
TicketList
tries to callgetAllTickets
when it mounts. However, initializing the database connection is asynchronous so the database is not ready on mount. When the DB is not initialized,getAllTickets
simply returns an empty array.How can you force the
getAllTickets
effect to re-run when the DB is initialized?Depending on state variables
There is a simple way to address this and its by specifying the
db
state as a dependency of the effect hook so that if its value changes, the hook re-runs.Since the
db
can beundefined
before its initialized, the effect callback needs to guard against this andreturn
early if that's the case. Now when you reload the page, tickets are listed as the effect waits untildb
becomes defined.However, you aren't done yet. When you create a new ticket, it doesn't get added to the list. Remember, IndexedDB is "external state" so you'll need to notify React when data changes to re-query the list of tickets.
Using effect hooks for notifications
The
useEffect
hook is perfect for this since it's designed to re-run its callback whenever its dependencies change.Can you think of a way to depend on ticket changes? Remember that you are tracking the state of ticket creation through the
TicketContext
now.By passing the
isCreating
state flag from theTicketContext
as a dependency to youruseEffect
hook, you can re-run thegetAllTickets
function wheneverisCreating
changes. Fantastic, now the newly created ticket will get added to the list as soon as its inserted into the database!It's also displayed underneath the list, but it would be useful to be able to select any ticket in the list to display instead of only the created ticket.
Currently that is managed within the
App
component using thecreatedTicket
state.Lifting state up to the Context
Sometimes in React you'll hear the term "lifting state up" which means refactoring a component to move its state higher in the component tree. You would do this in order to share that state more widely and to more tightly control a component's rendering.
Rather than only tracking the
createdTicket
, it could be better to track aselectedTicket
in the newly-introducedTicketContext
. That way, components can get or set which ticket is displayed at any time. You made some changes in multiple places, specifically:- Introduced a new context-level state variable for getting and setting
selectedTicket
- Refactored
App
to set the selected ticket to the newly-created ticket - Refactored
TicketList
to set the selected ticket when aTicketListItem
is clicked
You lifted the currently displayed ticket to the Context (and changed the name for clarity).
Should you lift `db` up to the Context?
If you noticed that there are two connection messages in the browser console now, you have a discerning eye. Hooks all have isolated state which means each instance of a hook within a component gets its own state. That means in this case, the
openDB
method is called twice -- once inApp
and once inTicketList
.Lifting
db
up to the Context won't automatically fix it (because there can be multiple Context providers in the tree). Instead, you'd want to make sure to only create one instance of thedb
variable. This is called using a singleton design pattern.In the Web Browser tab, you should now be able to click the summary of any ticket to display it below the list, and it should still display newly-created tickets after submission as well.
A working app
You can now create tickets, select any to display, and persist data across reloads. It seems your work is done!
After you submit your changes and request a code review, your team lead says:
Team Lead: Thanks for your hard work! I think there are still some optimizations we could make in places but we'll tackle those at a later time since the app is working fine. This looks great!
You now have a fully-functioning offline-capable helpdesk application! Congratulations!
- Introduced a new context-level state variable for getting and setting
-
Challenge
Recap and Summary
Recap
Here's what you learned in this lab:
- What hooks and side-effects are in React
- Implementing side effects using the
useEffect
hook - How to work with async code
- How to track (and reflect) async state
- How to write custom hooks
- How to use
createContext
anduseContext
- How to display dynamic data
- How to lift state up using the Context API
- Re-running effects with dependencies
This completes the foundational React you'll need to build highly-dynamic and complex web applications!
While the database you worked with for the lab only works in your local web browser, these same patterns and practices apply to any backend database or API you may integrate with in the real-world.
Stretch goals
If you play around with the demo application, you may notice a few things you could change:
- Only display
Loading tickets...
the first time tickets are fetched instead of every time after creating a ticket - Don't clear the form until after successfully creating a ticket
- Disable all the form fields during submission
- Allow selecting tickets through the URL
- Avoid connecting to the database twice using module-scoped state or moving the
db
state to theTicketContext
If you'd like to practice your React skills, apply everything you've learned through this lab series to see if you can accomplish these tasks.
Where to go next
After this Foundational series you are ready to dive deeper into intermediate to advanced React topics like:
- Reducing excessive re-rendering
- Memoization and the
useCallback
hook - Using state management solutions
- Suspense-enabled data fetching
- Refs and DOM manipulation
- Debugging with the React Dev Tools
- Writing React Server Components
These topics and many more are covered throughout other courses and guided labs in the React Learning Path.
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.