Managing State in React page
Work with React's useState Hook to manage a component's state.
Overview
State in React is a crucial concept, as it represents the parts of an app that can change over time. Each component can have its own state, allowing them to maintain and manage their own data independently. When the state changes, React re-renders the component and updates the DOM if it needs to.
There are different types of state within an application:
- Local State: This is data we manage in one or another component. Local state is often managed in React using the
useState
Hook, which we will cover in Objective 2 below. - URL State: The state that exists on our URLs, including pathname and query parameters. We already covered this in our section about Routing!
- Global State: This refers to data that is shared between multiple components. In React, global state can be managed using Context API or state management libraries; this is out of scope for this training.
Objective 1: Introducing React Hooks and useId
In this section, we will:
- Cover the fundamentals of React Hooks.
React Hooks
We’ve mentioned before that useState
is a Hook for managing state, but what
does that mean?
React Hooks (referred to as just Hooks for the rest of this training) are special functions that allow us to “hook” into React functionality. Hooks provide us with many conveniences like sharing stateful logic between components and simplifying what would be otherwise complex components.
We’ve actually already seen and used a Hook while building Place My Order! Do you remember this code from earlier?
import { Link, Outlet, useMatch } from 'react-router-dom';
import './App.css';
function App() {
const homeMatch = useMatch('/');
const restaurantsMatch = useMatch('/restaurants');
return (
<>
<header>
<nav>
<h1>place-my-order.com</h1>
<ul>
<li className={homeMatch ? 'active' : ''}>
<Link to='/'>Home</Link>
</li>
<li className={restaurantsMatch ? 'active' : ''}>
<Link to='/restaurants'>Restaurants</Link>
</li>
</ul>
</nav>
</header>
<Outlet />
</>
);
}
export default App;
The useMatch
Hook from react-router-dom
allowed us to check whether a given
path “matched” the current route.
React imposes several rules around the use of Hooks:
First, only call Hooks from functional React components or your own custom Hook.
Second, all the Hooks in a React function must be invoked in the same order every time the function runs, so no Hooks can occur after an
if
,loop
, orreturn
statement. Typically this means all Hooks are placed at the top of the React function body.Third, Hooks should be named by prefixing their functionality with
use
(e.g.useMatch
).
Hooks can only be used in functional components. Almost anything that could be done in a class component can be done with Hooks. The one thing that class component can do that Hooks cannot is implement error boundaries.
Setup
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to include the State and City dropdown lists.
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
const RestaurantList: React.FC = () => {
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {"(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
Verify
These tests will pass when the solution has been implemented properly.
✏️ Update src/pages/RestaurantList/RestaurantList.test.tsx:
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest';
import RestaurantList from './RestaurantList';
describe('RestaurantList component', () => {
it('renders the Restaurants header', () => {
render(<RestaurantList />);
expect(screen.getByText(/Restaurants/i)).toBeInTheDocument();
});
it('renders the restaurant images', () => {
render(<RestaurantList />);
const images = screen.getAllByRole('img');
expect(images[0]).toHaveAttribute('src', expect.stringContaining('2-thumbnail.jpg'));
expect(images[0]).toHaveAttribute('width', '100');
expect(images[0]).toHaveAttribute('height', '100');
expect(images[1]).toHaveAttribute('src', expect.stringContaining('4-thumbnail.jpg'));
expect(images[1]).toHaveAttribute('width', '100');
expect(images[1]).toHaveAttribute('height', '100');
});
it('renders the addresses', () => {
render(<RestaurantList />);
const addressDivs = screen.getAllByText(/Washburne Ave|Kinzie Street/i);
expect(addressDivs[0]).toHaveTextContent('2451 W Washburne Ave');
expect(addressDivs[0]).toHaveTextContent('Green Bay, WI 53295');
expect(addressDivs[1]).toHaveTextContent('230 W Kinzie Street');
expect(addressDivs[1]).toHaveTextContent('Green Bay, WI 53205');
});
it('renders the hours and price information for each restaurant', () => {
render(<RestaurantList />);
const hoursPriceDivs = screen.getAllByText(/\$\$\$/i);
hoursPriceDivs.forEach(div => {
expect(div).toHaveTextContent('$$$');
expect(div).toHaveTextContent('Hours: M-F 10am-11pm');
});
});
it('indicates if the restaurant is open now for each restaurant', () => {
render(<RestaurantList />);
const openNowTags = screen.getAllByText('Open Now');
expect(openNowTags.length).toBeGreaterThan(0);
});
it('renders the details buttons with correct links for each restaurant', () => {
render(<RestaurantList />);
const detailsButtons = screen.getAllByRole('link');
expect(detailsButtons[0]).toHaveAttribute('href', '/restaurants/cheese-curd-city');
expect(detailsButtons[1]).toHaveAttribute('href', '/restaurants/poutine-palace');
detailsButtons.forEach(button => {
expect(button).toHaveTextContent('Details');
});
});
it('renders the component', () => {
render(<RestaurantList />)
expect(screen.getByText('Restaurants')).toBeInTheDocument()
expect(screen.getByText('State:')).toBeInTheDocument()
})
it('allows state selection and updates cities accordingly', async () => {
render(<RestaurantList />)
const illinoisButton = screen.getByText('Illinois')
await userEvent.click(illinoisButton)
expect(screen.getByText('Current state: IL')).toBeInTheDocument()
expect(screen.queryByText('Choose a state before selecting a city')).not.toBeInTheDocument()
})
it('renders ListItem components for each restaurant', () => {
render(<RestaurantList />)
const restaurantNames = screen.getAllByText(/Cheese Curd City|Poutine Palace/)
expect(restaurantNames.length).toBe(2)
})
});
Exercise
- Associate the
<label>
and<select>
elements together using ID values provided by theuseId
Hook.
Having issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution
Click to see the solution
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to be:
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
const RestaurantList: React.FC = () => {
const [state, setState] = useState("")
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
const updateState = (stateShortCode: string) => {
setState(stateShortCode)
}
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} onClick={() => updateState(short)} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {state || "(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
Objective 2: Manage component state using Hooks
When users make choices that need to be maintained or that affect other parts of
the UI, we need to use state
. That's a pretty abstract concept so let's look
at a concrete example from the Place My Order Restaurants tab.
There are two dropdown lists — "State" which has a list of U.S. states, and another named "City" that displays a list of cities located in the selected "State" item. Until the user makes a choice in the "State" dropdown, the "City" dropdown has no additional options.
If “Illinois” is chosen in the “State” dropdown that value needs to persist
through component rendering and be available for use throughout the code:
getting the cities for Illinois, and enabling the “City” dropdown list. To
accomplish that the value “Illinois” is stored in React state
.
useState
We can store state that persists through component rendering with the useState
hook. You can set the initial state value when the component first renders
by providing the value as an argument to the Hook. If you do not provide a
value the initial state value will be undefined
.
This example shows a useState
Hook being set with an initial value of an empty
string.
import { ChangeEvent, useState } from 'react'
const NameField: React.FC = () => {
const [value, setValue] = useState<string>('')
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
}
return (
<label>
Name
<input onChange={handleChange} type="text" value={value} />
</label>
)
}
As you can see in the previous example, useState
returns an array with two
elements: the first is the current state value of the Hook, and the second is a
setter function that is used to update the state value. In the following code,
the value and setter are being used to update changes in a select component.
import { ChangeEvent, useState } from 'react'
const NameField: React.FC = () => {
const [value, setValue] = useState<string>('')
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
}
return (
<label>
Name
<input onChange={handleChange} type="text" value={value} />
</label>
)
}
Every time a useState
's setter is invoked with a new value, React compares the
new value with the current value. If the values are the same, nothing happens;
if the values are different, React will rerender the component so the new
state value can be used to update the component. In the example above, when the
user makes a selection, the List
component is rendered again, and the select
is updated with the current value.
ChangeEvent
ChangeEvent
is a type provided by React’s TypeScript definitions. It’s used to
type the event
object that you receive when an event occurs, like a change in
a form field. In TypeScript, when you're working with event handlers in React,
you often need to specify the type of the event object to get the full benefits
of TypeScript’s type checking.
The <HTMLInputElement>
part of ChangeEvent<HTMLInputElement>
specifies that
this change event is specifically for an HTML <input>
element. In HTML and the
DOM, different elements have different properties. By specifying <HTMLInputElement>
,
TypeScript knows that this event is for an <input>
element, and it will expect and
allow properties specific to an <input>
element, like value
, checked
, etc.
event.target.value
When an event occurs, such as a user typing in an input field, an event
object is
passed to the event handler function. This event object contains various properties
and methods that provide information about the event.
One such property is target
, which is a reference to the DOM element that triggered
the event. In the case of an input field, target would refer to the specific <input>
element where the user is typing.
The property event.target.value
is particularly important when dealing with input
fields. The value property here holds the current content of the input field. It
represents what the user has entered or selected. When you access event.target.value
in your event handler function, you’re essentially retrieving the latest input
provided by the user. This is commonly used to update the state of the component with
the new input value, ensuring that the component’s state is in sync with what the user
is entering. In a TypeScript context, this process not only manages the state
dynamically but also ensures type safety, making your code more robust and less prone
to errors.
Setup
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to be the following:
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
const RestaurantList: React.FC = () => {
const [state, setState] = useState("")
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const cities = [
{ name: 'Madison', state: 'WI' },
{ name: 'Springfield', state: 'IL' },
]
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
const updateState = (stateShortCode: string) => {
setState(stateShortCode)
}
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} onClick={() => updateState(short)} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {state || "(none)"}
</p>
</div>
<div className="form-group">
City:
{state ? cities.map(({ name }) => (
<button key={name} type="button">
{name}
</button>
)) : <> Choose a state before selecting a city</>}
<hr />
<p>
Current city: {"(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
Verify
These tests will pass when the solution has been implemented properly.
✏️ Update src/pages/RestaurantList/RestaurantList.test.tsx to be the following:
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest';
import RestaurantList from './RestaurantList';
describe('RestaurantList component', () => {
it('renders the Restaurants header', () => {
render(<RestaurantList />);
expect(screen.getByText(/Restaurants/i)).toBeInTheDocument();
});
it('renders the restaurant images', () => {
render(<RestaurantList />);
const images = screen.getAllByRole('img');
expect(images[0]).toHaveAttribute('src', expect.stringContaining('2-thumbnail.jpg'));
expect(images[0]).toHaveAttribute('width', '100');
expect(images[0]).toHaveAttribute('height', '100');
expect(images[1]).toHaveAttribute('src', expect.stringContaining('4-thumbnail.jpg'));
expect(images[1]).toHaveAttribute('width', '100');
expect(images[1]).toHaveAttribute('height', '100');
});
it('renders the addresses', () => {
render(<RestaurantList />);
const addressDivs = screen.getAllByText(/Washburne Ave|Kinzie Street/i);
expect(addressDivs[0]).toHaveTextContent('2451 W Washburne Ave');
expect(addressDivs[0]).toHaveTextContent('Green Bay, WI 53295');
expect(addressDivs[1]).toHaveTextContent('230 W Kinzie Street');
expect(addressDivs[1]).toHaveTextContent('Green Bay, WI 53205');
});
it('renders the hours and price information for each restaurant', () => {
render(<RestaurantList />);
const hoursPriceDivs = screen.getAllByText(/\$\$\$/i);
hoursPriceDivs.forEach(div => {
expect(div).toHaveTextContent('$$$');
expect(div).toHaveTextContent('Hours: M-F 10am-11pm');
});
});
it('indicates if the restaurant is open now for each restaurant', () => {
render(<RestaurantList />);
const openNowTags = screen.getAllByText('Open Now');
expect(openNowTags.length).toBeGreaterThan(0);
});
it('renders the details buttons with correct links for each restaurant', () => {
render(<RestaurantList />);
const detailsButtons = screen.getAllByRole('link');
expect(detailsButtons[0]).toHaveAttribute('href', '/restaurants/cheese-curd-city');
expect(detailsButtons[1]).toHaveAttribute('href', '/restaurants/poutine-palace');
detailsButtons.forEach(button => {
expect(button).toHaveTextContent('Details');
});
});
it('renders the component', () => {
render(<RestaurantList />)
expect(screen.getByText('Restaurants')).toBeInTheDocument()
expect(screen.getByText('State:')).toBeInTheDocument()
})
it('allows state selection and updates cities accordingly', async () => {
render(<RestaurantList />)
const illinoisButton = screen.getByText('Illinois')
await userEvent.click(illinoisButton)
expect(screen.getByText('Current state: IL')).toBeInTheDocument()
expect(screen.queryByText('Choose a state before selecting a city')).not.toBeInTheDocument()
})
it('allows city selection after a state is selected', async () => {
render(<RestaurantList />)
const illinoisButton = screen.getByText('Illinois')
await userEvent.click(illinoisButton)
const greenBayButton = screen.getByText('Springfield')
await userEvent.click(greenBayButton)
expect(screen.getByText('Current city: Springfield')).toBeInTheDocument()
})
it('renders ListItem components for each restaurant', () => {
render(<RestaurantList />)
const restaurantNames = screen.getAllByText(/Cheese Curd City|Poutine Palace/)
expect(restaurantNames.length).toBe(2)
})
});
Exercise
- The selected state value should be managed by a
useState
Hook. - The selected city value should be managed by a
useState
Hook. - The City select should only include cities for the selected state.
Having issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.
Solution
Click to see the solution
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to be:
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
const RestaurantList: React.FC = () => {
const [state, setState] = useState("")
const [city, setCity] = useState("")
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const cities = [
{ name: 'Madison', state: 'WI' },
{ name: 'Springfield', state: 'IL' },
].filter(city => {
return city.state === state
})
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
const updateState = (stateShortCode: string) => {
setState(stateShortCode)
setCity("")
}
const updateCity = (cityName: string) => {
setCity(cityName)
}
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} onClick={() => updateState(short)} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {state || "(none)"}
</p>
</div>
<div className="form-group">
City:
{state ? cities.map(({ name }) => (
<button key={name} onClick={() => updateCity(name)} type="button">
{name}
</button>
)) : <> Choose a state before selecting a city</>}
<hr />
<p>
Current city: {city || "(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
Objective 3
TODO
Key concepts
TODO
Concept 1
TODO
Concept 2
TODO
Setup
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to be:
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
import { useCities } from '../../services/restaurant/hooks'
const RestaurantList: React.FC = () => {
const [state, setState] = useState("")
const [city, setCity] = useState("")
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const cities = [
{ name: 'Madison', state: 'WI' },
{ name: 'Springfield', state: 'IL' },
].filter(city => {
return city.state === state
})
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
const updateState = (stateShortCode: string) => {
setState(stateShortCode)
setCity("")
}
const updateCity = (cityName: string) => {
setCity(cityName)
}
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} onClick={() => updateState(short)} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {state || "(none)"}
</p>
</div>
<div className="form-group">
City:
{state ? cities.map(({ name }) => (
<button key={name} onClick={() => updateCity(name)} type="button">
{name}
</button>
)) : <> Choose a state before selecting a city</>}
<hr />
<p>
Current city: {city || "(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
✏️ Create src/services/restaurant/hooks.ts and update it to be:
import type { City } from './interfaces'
export function useCities(state: string): City[] {
}
✏️ Create src/services/restaurant/interfaces.ts and update it to be:
export interface City {
name: string
state: string
}
export interface State {
name: string
short: string
}
Verify
✏️ Create src/services/restaurant/hooks.test.ts and update it to be:
import { renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useCities } from './hooks';
describe('useCities Hook', () => {
it('should return cities from Wisconsin when state is WI', () => {
const { result } = renderHook(() => useCities('WI'));
expect(result.current).toHaveLength(1);
expect(result.current[0].name).toBe('Madison');
});
it('should return cities from Illinois when state is IL', () => {
const { result } = renderHook(() => useCities('IL'));
expect(result.current).toHaveLength(1);
expect(result.current[0].name).toBe('Springfield');
});
it('should return no cities for an unknown state', () => {
const { result } = renderHook(() => useCities('CA'));
expect(result.current).toHaveLength(0);
});
});
Exercise
TODO
Solution
Click to see the solution
✏️ Update src/pages/RestaurantList/RestaurantList.tsx to be:
import CheeseThumbnail from 'place-my-order-assets/images/2-thumbnail.jpg'
import PoutineThumbnail from 'place-my-order-assets/images/4-thumbnail.jpg'
import { useState } from 'react'
import ListItem from './ListItem'
import { useCities } from '../../services/restaurant/hooks'
const RestaurantList: React.FC = () => {
const [state, setState] = useState("")
const [city, setCity] = useState("")
const states = [
{ name: 'Illinois', short: 'IL' },
{ name: 'Wisconsin', short: 'WI' },
]
const cities = useCities(state)
const restaurants = {
data: [
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: CheeseThumbnail,
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: PoutineThumbnail,
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
]
};
const updateState = (stateShortCode: string) => {
setState(stateShortCode)
setCity("")
}
const updateCity = (cityName: string) => {
setCity(cityName)
}
return (
<>
<div className="restaurants">
<h2 className="page-header">Restaurants</h2>
<form className="form">
<div className="form-group">
State:
{states.map(({ short, name }) => (
<button key={short} onClick={() => updateState(short)} type="button">
{name}
</button>
))}
<hr />
<p>
Current state: {state || "(none)"}
</p>
</div>
<div className="form-group">
City:
{state ? cities.map(({ name }) => (
<button key={name} onClick={() => updateCity(name)} type="button">
{name}
</button>
)) : <> Choose a state before selecting a city</>}
<hr />
<p>
Current city: {city || "(none)"}
</p>
</div>
</form>
{restaurants.data ? (
restaurants.data.map(({ _id, address, images, name, slug }) => (
<ListItem
key={_id}
address={address}
name={name}
slug={slug}
thumbnail={images.thumbnail}
/>
))
) : (
<p>No restaurants.</p>
)}
</div>
</>
)
}
export default RestaurantList
✏️ Update src/services/restaurant/hooks.ts to be:
import type { City } from './interfaces'
export function useCities(state: string): City[] {
const cities = [
{ name: 'Madison', state: 'WI' },
{ name: 'Springfield', state: 'IL' },
]
return cities.filter(city => {
return city.state === state
})
}
Next steps
TODO