Goal
Using React with TypeScript we’ll consume a GraphQL API using Apollo and build a simple site.
This site will have different pages so the React Router will be set in place and the entire project will be supported by Vite.
GraphQL API
I’ll use the API built on the article GraphQL with Absinthe on Phoenix - Authentication, but feel free to use any other API.
React App
We’ll use the Vite to create the bootstrap since it can create smaller packages and can be faster to deal with during the development.
First, make sure you have NodeJS and Yarn installed.
node --version
yarn --version
Then let’s create the app’s skeleton called app
using React and TypeScript:
yarn create vite app --template react-ts
You can run the following commands:
yarn
yarn dev
And test it at http://localhost:5173
Cleanup
Let’s remove some unused files:
rm src/App.css
rm src/index.css
rm public/vite.svg
rm src/assets/react.svg
Entrypoint
Edit the index.html
and let it clean to keep the things easy to understand:
<!doctype html>
<html>
<head>
<title>Site with React and TypeScript and Vite and Apollo</title>
</head>
<body>
<div id="root"></div>
<script src="/src/main.tsx" type="module"></script>
</body>
</html>
Here we have the script /src/main.tsx
that starts everything and renders the content inside the root
div.
The content of main.tsx
should be:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
If you’re thinking that the !
is strange, you’re right, it’s strange, but it happens because we’re using a tsx
, a TypeScript extension for a React file jsx
. Since createRoot
must receive an element, the exclamation says: Relax, it’ll exists, I ensure.
Here the ReactDOM
creates the root element and renders inside it the App
component wrapped by a helper that acts like a linter for our entire app, so it’s a good idea to keep it.
The App
The entry point component calls App
and from there we’ll call all other components:
// src/App.tsx
import Home from "./components/Home";
const App = () => (
<div style={{ maxWidth: 1800 }}>
<Home message="Welcome to the Bible" />
</div>
);
export default App;
In the past were commonly used components as a class, but nowadays we normally create components as a simple function.
Let’s see a quick review of how React works:
The App
, the main function, returns a div
with a style max-width: 1800px
. In fact <div>
here is not an HTML element, but a React function that creates the div element. That’s why it can receive attributes using the dynamic tag {}
. Inside it, we can use JS code like the hash { maxWidth: 1800 }
that will become the style attributes.
The components can be compounded by another component, so we’re rendering the Home
component.
Component
The Home
element is called a component and it returns just the provided message:
// src/components/Home.tsx
type HomeProps {
message: string,
}
const Home = ({ message }: HomeProps) => (
<div>{message}</div>
);
export default Home;
Using TypeScript we can specify the type used on the Home
component, so the attributes of it must follow the specified attributes.
Ok, just test your code at http://localhost:5173.
Types
Before we continue, let’s agree that we’ll put all types inside the types
folder:
// src/types/Home.tsx
export type HomeProps = {
message: string,
}
// src/types/Book.tsx
export type BookProps = {
id: number,
name: string,
position: number,
}
If the linter complains, just add the /* eslint-disable @typescript-eslint/no-unused-vars */
on the top of the file.
Books
This component should receive a list of books, interact with each one and display it.
// src/components/Books.tsx
import { BookProps } from "../types/Book";
const items = [
{ id: 1, name: 'Book 1', position: 1 },
{ id: 2, name: 'Book 2', position: 2 },
{ id: 3, name: 'Book 3', position: 3 },
]
const Books = () => {
return (
<ul>
{items.map((item: BookProps) => {
return (
<li key={item.id}>
<a href="#">{item.name}</a>
</li>
)
})}
</ul>
);
}
export default Books;
Let’s add it to the App
:
// src/App.tsx
import Books from "./components/Books";
import Home from "./components/Home";
const App = () => (
<div style={{ maxWidth: 1800 }}>
<Home message="Welcome to the Bible" />
<Books />
</div>
);
export default App;
Currently, we’re using fake data, but how can we fetch it from an API?
Apollo Client
Apollo Client allows us work with GraphQL on the client side, let’s install it:
yarn add @apollo/client graphql
On the main.tsx
file we need to configure the client access:
// src/main.tsx
// ...
import { ApolloProvider, ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
const host = import.meta.env.VITE_API_HOST;
const token = import.meta.env.VITE_API_TOKEN;
const authLink = setContext((_, { headers }) => {
return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '' } };
});
const httpLink = createHttpLink({ uri: host });
const client = new ApolloClient({ cache: new InMemoryCache(), link: authLink.concat(httpLink) });
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
)
First, we get the host of the API and the auth token, an JWT, to allow us to access the API:
# .env
VITE_API_HOST=http://localhost:4000
VITE_API_TOKEN=eyJhbGciOiJIUzUxMiJ9.eyJhdWQiOiJodHRwczovL3d3dy53Ym90ZWxob3MuY29tIiwiZXhwIjoxNzIxNzg4MDI4LCJpYXQiOjE2OTAyNTIwMjgsImlzcyI6Imh0dHBzOi8vd3d3Lndib3RlbGhvcy5jb20iLCJqdGkiOiI4ZWY5MGExODYxNjg4MmFlYThjMmQwOTZmZDNhM2ZlNyIsIm5iZiI6MTY2ODExNjk3OSwic3ViIjoiYXBpIiwidXNlcl9pZCI6MX0.pWDmhJjxkQglVNIgxz9Fgc8dm4zVS8HKs7ls7Elp-RbBxpX1kG2iGlUTFojPYAbdETbmGeDtoWgHau6aD_oWbQ
Then an auth link is created adding the authorization
header key and returning a `ApolloLink2`` type.
This type allows us concat another link containing the URL where the API lives. And finally, we create the Apollo Client using an in-memory cache and the link created before.
Ok, now we’re ready to wrap the application with <ApolloProvider>
component to allow the application to fetch an API via the created client
.
GraphQL Queries
Apollo Client allows us to create queries to be executed from any place we want. Here is an query to fetch all books:
// src/graphql/queries/book.tsx
import { gql } from '@apollo/client';
const BOOKS_QUERY = gql`
query books($limit: Int!) {
books(limit: $limit) {
id
name
position
}
}
`;
export { BOOKS_QUERY };
Using the gql
we can easily declare GraphQL queries.
To use it just add the following code to the `Books`` component:
// src/components/Books.tsx
// ...
import { BOOKS_QUERY } from '../graphql/queries/Book';
import { useQuery } from '@apollo/client';
const { loading, error, data } = useQuery(BOOKS_QUERY, {
variables: { limit: 100 },
});
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error.message}</p>;
}
return (
<ul>
{data.books.map((item: BookProps) => {
return (
<li key={item.id}>
<a href="#">{item.name}</a>
</li>
)
})}
</ul>
);
The useQuery
executes the query and returns three variables indicating if it is loading, the error when it exists and the data when it executes with success. We early return different data to be displayed on the screen based on it and finally, we can get the result inside data.books
keeping the same logic as before.
Test your code and it should work properly now.
Routes
We don’t want the Home page living together with the books page. To allow navigation we can use the React Router:
yarn add react-router-dom
Now we create a component to display our links:
// src/components/Navbar.tsx
import { Link } from 'react-router-dom';
const Navbar = () => (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/books">Books</Link></li>
</ul>
);
export default Navbar;
Just including this component on the app still won’t work because we must bind each Link
path, declared in the to
attribute to a specific component, so our App
will be like this:
// ...
import Navbar from './components/Navbar';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const App = () => (
<div style={{ maxWidth: 1800 }}>
<BrowserRouter>
<Navbar />
<Routes>
<Route path="/" element={<Home message="Welcome to the Bible" />} />
<Route path="/books" element={<Books />} />
</Routes>
</BrowserRouter>
</div>
);
export default App;
As you can see the Navbar
containing the links must be inside the BrowserRouter
to work and then we declare all route’s binds. When the link to /
booksis accessed the
` component will be displayed. Simple, right?
Conclusion
These days it’s very simple to work with React, the functional way makes it simpler using just simple functions, the API is fetched very easily with Apollo and work with routes is dead simple too.
Of course, we should use a back-end when we want to hide the JWT token and when we need to use more complex components or form we need to deal with state and more libraries, but I ensure you, it’s much simpler than the old days working with HTML interpolated in the JS or even better the HandlebarsJS.
Repository link: https://github.com/wbotelhos/site-with-react-and-typescript-and-vite-and-apollo
Any suggestion? Please, send me an email here.