Week 7 - React Router
[Recap] Custom Components
Custom (functional) Components are created simply by exporting a function. A quick review of the requirements:
- The function name must start with a capital letter
- ex,
app
isn’t valid butApp
is
- ex,
- The function must return JSX with a single element
- Remember, you can wrap your contents in a fragment (
<> ... </>
) to create a single root elemeent - The return value is a mix of HTML elements and React components
- Remember, you can wrap your contents in a fragment (
- Only one component should exist per file, and the custom component should be the default export
- The first argument to the function is the component props
- Values are passed to this like HTML attributes. Ex,
<App property=value
- Props are typically destructured.
- Destructed:
function App({ property }) { console.log(property) }
- Not destructed
function App (props) { console.log(props.property) }
- Destructed:
- Types on props are optional for the purpose of this class, but they do help!
- The easiest way to specify a type is shown in the example below
- Values are passed to this like HTML attributes. Ex,
- Curly-braces (
{ }
) within JSX represent javascript code- This allows rendering a simple variable, like
{data.name}
- This allows access to all the JS functions we’ve used so far, like
map
,filter
, etc:data.map(d => <Link to={d} />)
- This allows rendering a simple variable, like
- Rendering data in a loop requires a unique
key
property to be passed
// Comment data type - fields from https://jsonplaceholder.typicode.com/comments/1
export type User = {
id: number;
first_name: string;
last_name: string;
phone: string;
email: string;
};
// props data type - an object with an array of users passed as "contacts"
type Props = {contacts: User[]}
// destructured | var
// variable | type
// vvvvvvvvvvvv |vvvvv
export default function CardGallery({ contacts }: Props) {
return (
// Returning fragment so we have a single element returned
<>
{/* Accessing variable */}
<div>Number of contacts: {contacts.length}</div>
<Gallery hasGutter>
{/* Accessing Array.map function to translate */}
{/* comment list to List of Comment cards */}
{contacts.map((u) => (
// unique key user prop accepts
// is user.id full user
<UserCard key={u.id} user={u} />
))}
</Gallery>
</>
);
}
React Router
React is really just a bunch of javascript running in the browser - there is no actual “page” to be loaded on the server when navigating to a different URL.
To render different content based on the current page URL, we have to implement a “router” within our Javascript code. We’ll be using React Router for this.
What even is a path?
In traditional web servers, loading a different path would download unique HTML file. For example, /hello.html
and /world.html
would be two totally different files, just like if we were accessing the filesystem of the server.
Many modern websites are called “Single Page Applications”, or SPA. This means that no matter what path is loaded, the same files are always loaded. Paths represent different content as defined by the Javascript code executing in the browser.
The same “bundle of code” is loaded for /hello
, /world
, /hello/world
, or /asdf
. Once the Javascript bundle is loaded into the browser, it can determine if it knows what the user is looking for.
If it doesn’t know, like in the case of /asdf
, an error page would be displayed.
Showing Dynamic Content
Paths can contain dynamic sections, allowing your code to receive a variable referencing the content to load (usually a unique identifier).
The path /users/:id/profile
had one dynamic section - /:id/
We would use this to fetch a specific user from our data source (API, data file, etc) and show that user’s information.
By including the :id
in the url, we create a url which can be bookmarked or shared between users of the platform to reference specific information, rather than manually navigating to that user on each page load.
React Router
React Router has (at-least) 3 main ways to be setup:
- Data Mode
- Framework Mode
- Declarative Mode
We’ll be using Data mode for the final project. This allows us to define our routes as an object:
import { createBrowserRouter, RouterProvider } from 'react-router';
// First we define our routes using createBrowserRoute
createBrowserRouter([
{
// for the path `/`
index: true,
// render the component <Home />
element: Home
},
{
// for the path `/about`
path: "about",
// render the component <About />
element: About
},
])
createRoot(document.getElementById('root')!).render(
<StrictMode>
{/* Then we use RouterProvider to render each route */}
<RouterProvider router={router} />
</StrictMode>,
)
Path is what we would type into the browser url, after the TLD (top level domain). Ex, in the url https://docs.ci256.cloud/week1
, the path is /week1
element
is the thing we want to render when loading the specified path.
Each route can optionally have several children routes. A child route will be prefixed by any paths defined by it’s parent.
createBrowserRouter([
// path: `/`
{ index: true, element: Home },
// path: `/about`
{ path: "about", element: About },
{
// path: `/auth`
path: "auth",
Component: AuthLayout,
children: [
// path: `/auth/login`
{ path: "login", element: Login },
// path: `/auth/register`
{ path: "register", element: Register },
],
},
])
Layout Components
“Layout Components” are used to define a common layout for everything below a certain route - this is useful for defining things like a sidebar or navbar.
Defining a “Parent” with a Component
but not a path will make that route a Layout:
const router = createBrowserRouter([
{
// App is used as a Layout here
Component: App,
children: [
// The layout is used to render the paths
{
path: "/",
element: <Box sx={{bgcolor: 'white', p: 2, borderRadius: 6}}>
<Typography>Select a contact to get started</Typography>
</Box>,
},
{
path: "/users/:id",
element: <DisplayContact />,
},
]
}
]);
We need to include the special component <Outlet />
from React Router in any Layout Component we define, so React knows where to render the children routes within the layout.
import { Outlet } from 'react-router';
<Box sx={{flexGrow: 2, background: 'blue'}}>
<Box sx={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%'}}>
{/* This Outlet in our App.tsx is required to render children */}
<Outlet />
</Box>
</Box>
Data Loading
We often want to load some specific data to display with our component. To do this, we will use the loader
argument to our router definition, which accepts an async function. We must use the loader
function to load data async, as we can’t have an async call in the top level of a component - this will become relevant next class.
The loader function is passed a single parameter, containing a few items within it. The one we really care about is params
, which is an object with and custom :params
we defined in our path.
const router = createBrowserRouter([
{
path: "/users/:id",
element: <DisplayContact />,
// pass a loader function to load our contact data for us
loader: async ({params}) => {
// params is an object with our :id argument as a string.
// Ex, if we loaded the path /users/3 the params object would be
// { id: '3' }
console.log('loading contact with id', params.id)
// we call a custom getContact() function with the ID
return await getContact(params.id)
}
},
]);
Inside our component, we can use our loaded data with a special hook called useLoaderData()
, which returns anything we return from the loader()
function.
export default function DisplayContact() {
// this is like calling the function we pass to loader()
// aka, this is like calling getContact(params.id)
const contact = useLoaderData();
We are no longer guaranteed to have a contact in this state - for example, if a user were to load the url /users/asdf
or /users/999
.
We should now be performing a check for contact to exist. Remember that whatever we return from a component function is what is rendered - meaning we can simply return early if the contact does not exist.
const contact = useLoaderData();
if(!contact) {
return <Box>Contact not found.</Box>
}
return (
<Card>
...
</Card
)
SPA-like Navigation
Normally when you click an href
link in the browser, it reloads the entire page. In our SPA app however, we already know every page loads the same javascript bundle - so it’s a waste to reload the page when clicking a link!
To tell React Router to load the new page, instead of allowing the browser to reload, we use the special <Link>
component from React Router.
import { Link } from 'react-router';
// we use <Link to=...
<Link to={`/users/create`}>Create User</Link>
// instead of <a href=...>
<a href="/users/create">Create User</Link>
NOTE! Our component library
joyui
also has a<Link>
component that does not work the same way! Be careful to import from react-router and notjoyui
Lab Instructions
Lab tonight will be updating your midterm project to utilize React Router. The requirements are as follows:
- Load contact details by url:
/users/:id
- Update the sidebar buttons to utilize
Link
from ReactRouter- this is a change from
useState
- this is a change from
- Add an async function
getContact
tocontacts.ts
- Utilize
getContact
in a React Router loader function. - Handle when
useLoaderData()
doesn’t return a contact
Note: We’re going to break our ability to Add Contacts. Don’t worry, we’ll fix this in the coming weeks!
You will be graded on tonight’s lab. Please show me in class that the lab is complete - you may show me next week if you need additional time to work on it.
1. Utilize React Router
First, install React Router from the command line with this command:
npm install react-router-dom
Then update main.tsx
to utilize React Router by creating a router object with createBrowserRouter
const router = createBrowserRouter([
{
path: "/",
element: <Box>Hello, World!</Box>,
},
]);
Then mount <RouterProvider />
in place of your <App />
component. Be sure to pass router
as a prop.
Loading your app should now show Hello, World!
and nothing else.
2. Add Layout
Update the router configuration to utilize the App component as a Layout component
Don’t forget to update App and replace your
<DisplayCard>
or similiar component with<Outlet />
!
Loading your App should display your full la render the pathyout, with Hello, World
in the middle of the screen.
If it is displaying a contact card, you are not done! You still need to replace your contact card with <Outlet />
3. Update your Sidebar to utilize Links
Update the sidebar to utilize links instead of an onClick
listener. When a contact is clicked on the left, your browser url should update to /users/:id
, where :id
is the actual id of the contact - ex, /users/1
.
Remember to use the
Link
from React Router! You know you’re doing it right if you pass the propto=
and nothref=
.
When you click on a contact, you should be present with a 404 - page not found error. We’ll fix this in the next step!
4. Add a route for displaying contacts
Add a new route /users/:id
for displaying a specific contact by ID.
First, you’ll want to create an async function to load a contact by ID. Inside of contacts.ts
, create a function getContact
which accepts a single parameter, id
. Return the requested contact, or null
/undefined
if the contact is not found.
Then, create a loader function on the new route to call getContact(params.id)
.
Finally, update your DisplayContact
component (your name may be different) to utilize the loaded contact using useLoaderData()
.
Now, when you click a contact in the sidebar, you should be presented with the contact’s details.
5. Error Handling when contact does not exist
If useLoaderData()
doesn’t return a contact, handle that case! Display something like “contact not found”.
6. Update your index route
Update the index route to include something like “Select a contact to get started”, with reasonable styling, instead of “Hello, World”