Table of Contents
This example project provides a UI to interact with the Author Books GraphQL Server.
It allows a user to create an author
or book
and assign an author
to a book
or any number of books
to an author
. Users can also remove an author
from a book
or remove a book
from an author
. An author
or book
can be updated
or deleted
and a list of book
s or author
s can be viewed.
There were several reasons for working on this project, including the chance to:
- build out a React application
- use Apollo Client for state management
- explore error handling strategies
- implement a comprehensive testing suite
Below is a list of major frameworks/libraries that were used to bootstrap this project.
Google fonts were downloaded and stored within the application and served up to the user.
Once downloaded a free online font converter was used to convert .ttf
into .woff
and .woff2
font formats.
These files were then copied to src/fonts/
where webpack can dynamically bundle them.
In indexStyles.css
, @font-face was used to map these custom fonts and applied to the body
tag via font-family
. Finally, these fonts were imported into index.tsx
.
There are several approaches that can be considered when determining how to handle errors that occur within the application:
- handle errors directly in each component via
error
object returned by query or mutation
const [something, { loading, error, data }] = useQuery(SOMETHING);
- create an
ErrorMessage
component to abstract away the different errors but still handle directly in each component by passingerror
object as aprop
if (error.networkError) {
// handle
}
if (error.graphQLErrors.length > 0) {
// handle
}
if (error.clientErrors.length > 0) {
// handle
}
<ErrorMessage error={error}
- create React Error Boundaries
class ErrorBoundary extends React.Component {
/* ... */
}
<ErrorBoundary>{/* App Component / Components */}</ErrorBoundary>;
- create a custom error context (plus
provider
andhook
) to control global errors across the application, such asErrorContext.tsx
<ErrorContextProvider>{/* App Component / Components */}</ErrorContextProvider>;
function AppComponent() {
const { errorMessage, setErrorMessage } = useErrorContext();
}
- create custom hook wrappers around default Apollo queries and mutations that handle errors -
apolloWrappers.ts
. Then updategraphql-codegen.yml
to include theapolloReactHooksImportFrom
field that points toapolloWrappers.ts
. Add an<entityName.graphql
file that includes a query, and / or mutation. When the codegen is ran, it will produce implementations of these queries, lazy queries and mutations that include the custom error handling. These can then be referenced in the application.
query Something() {
something() {
id
}
}
const useQuery = function (query, options) {
function showError() {
// update state to render error component
// log error message
}
const onError = useCallback((error) => showError(error), [showError]);
return apolloUseQuery(query, { onError, ...options });
};
// instead of Apollo default
const query = useQuery(SOMETHING);
// can use custom implementation
const query = useSomethingsQuery();
- use React Error Boundary library
To set up generating schema based on an external source, the package.json
was updated to include a graphql-download-local
script to use Apollo Rover that introspects the external schema and outputs results to schema.graphql
.
A graphql-codegen.yml
file was then added and the package.json
updated to include a graphql-codegen
script that looks at the schema.graphql
file and outputs a generated/graphql-types.tsx
file
Once the schema is created and codegen ran, these types can be referenced throughout the application.
- Create Author:
- When modal is open and invalid input is provided then validation errors will be displayed when pressing the "create" button
- Valid input will result in a form submit when pressing the "create" button or pressing the enter key
- Create Books:
- When modal is open and invalid input is provided then "create" button is disabled
- Valid input will result in a form submit when pressing the "create" button or pressing the enter key
- Author Search Books:
- this uses a lazy query to query books when the "search Books" button is pressed
- data from lazy query is filtered for books that are not associated to an author
- when a book is selected and author is updated, AuthorPage is re-rendered causing lazy query data to be updated thus displaying the correct filtered books in the dropdown
- Remove Author From Book:
- since the
REMOVE_BOOK_AUTHOR
FE query looks like this:
removeBookAuthor(input: $input) { id author { id } }
- the response from the mutation does not provide a way to update the associated author's books to remove this book from the associated author
- therefore, an
update
function is used to handle this cache update manually, instead of refetchingAUTHORS
query
- since the
Apollo Client is a state management library that simplifies managing remote and local data with GraphQL. It allows you to fetch, cache, and modify application data, all while automatically updating UI.
This application has implemented several strategies to manage data as a way to provide some examples of approaches that you can take when building your application.
- For queries:
- working directly with
loading
,error
anddata
states - using
lazyQuery
to only fetch data when needed fetchPolicy
to set initial and subsequent data fetching strategyonCompleted
to set state when query successfully completes@client
directive for author'sfullName
field
- working directly with
- For mutations:
refetchQueries
to refetch data from server to keep cache in syncupdate
to manually handle cache update from mutation
- For Type Polices:
Author
books field custom logic added
There are several things that this app has not included an implementation, including but not limited to:
- queries:
pollInterval
errorPolicy
initialFetchPolicy
nextFetchPolicy
refetch
skip
- mutations:
- default
variables
that are used when a mutation is called without these variables optimisticResponse
to update UI ahead of updates being made to server and receiving a successful responseonQueryUpdated
to see what cached fields were updated during an update and maybe fetch data
- default
Apollo client provides plenty of features, 2 of which have been outlined below:
TypePolicy
- a
typePolicy
was added forauthor.books
to ensure we return incoming results from the query - this fixes the "removing book from author" warnings in the
console
- this fixes the "adding a book to author" so the UI reflects the newly added book
- a
@client Directive
- The
@client
directive can be used to inform Apollo Client not to include a<field>
in the query it sends to the server - In our application, the
fullName @client
field exists that will not sendfullName
to the server to resolve but instead be resolved either from, a) a local resolver function, or b) the Apollo Client cache - In this application, a
fullName @client
has been added to bothAuthors
andAuthor
queries (authors.graphql
andauthor.graphql
) - In order to handle the
fullName
field via the application's codegen process, aschema-local.graphql
file was created to include code that extendsAuthor
to include thefullName
field - The
graphql-codegen.yml
file was also updated to include the newschema-local.graphql
file afterschema.graphql
in theschema
field - The
schema.graphql
is generated from downloading the schema from an external source while theschema-local.graphql
is locally and manually updated - The order for these file inclusions is important since
schema-local.graphql
is extendingAuthor
fromschema.graphql
soschema.graphql
needs to exist first - Once the
schema.graphql
is downloaded and codegen ran, TypeScript will now recognizeAuthor.firstName
- A local resolver was then added to
index.tsx
to return a value forAuthor.fullName
, which can then be used inside of components, for example inAuthorsPage.tsx
- The
This application includes, unit, visual regression and end-to-end testing.
- Unit
- Jest is used since this comes out of the box with "Create React App"
- Apollo Client Mock Provider is also used since that is the recommended way to work with Apollo Client when testing
- make sure you are handling query and mutation
errors
since when testing withMockProvider
errors can be swallowed and therefore, tests pass but query / mutations may not actually be invoked as you would expect
- make sure you are handling query and mutation
- in
testUtils
a helper method has been implemented to allow a query and / or mutation to be resolved before asserting against the DOM- see
renderWithApolloAndRouter
for implementation of helper method that sets up the test structure to be the same as the application structure, including, providers, routes etc
- see
- Storybook
- has been implemented for different types of components in 2 ways:
- visual UI where a snapshot of the component is created for each state it can be in
- using the
play
functionality to perform user actions and assert against expected behavior
- one thing to note is that the
controls
do not reset automatically when you change from one story back to another. A reset button in the table does contain a reset button the top right that will reset to default values when pressed - in order to test coverage, you need to have
storybook
running in one terminal window and then run the coverage command in another terminal window
- has been implemented for different types of components in 2 ways:
- Playwright
- allows use to perform testing across the application in a way that the user would perform actions
- does not come with an in-built "watch" mode but ths app installed a open source watch library whic allows for debugging
- allows the use of
debug
andawait page.pause()
to step through and pause code
- allows the use of
- when performing these tests the app uses a mock pattern to intercept the request and return a mocked response. It is possible for these tests to not use mocks but would need the FE to be connected to a running BE server
- to aid in the handling of mock GraphQL requests, a custom method has been created that looks at the requests'
operationName
andvariables
and provide dedicated responses - in order to handle a current issue with ES Lint and Storybook, an update to
package.json
was made in theeslintConfig
overrides section to disable an eslint rule for Playwright to avoid the warning "Avoid destructuring queries fromrender
result, usescreen.<nameOfMethod>
instead"
The following information will provide you with the details necessary to get the application up and running locally.
On your operating system of choice, ensure that NodeJS version 18.12.0
is installed. It is recommended that a Node Version Manager be used, such as NVM. When installing NodeJS
this way, the correctly associated npm
version should automatically be installed.
nvm install node@18.12.0
Once NodeJS
and npm
is installed you can follow these steps:
- Clone the repo
git clone https://github.com/DeanGilewicz/ab-web-client.git
- Install NPM packages
npm i
- Download schema
npm run graphql-download
- Run codegen
npm run graphql-codegen
- Run the application
npm start
The application provides a variety of commands in package.json
:
- start
- runs the application locally
- build
- builds an optimized production application
- test
- runs Jest unit tests
- test:coverage"
- collects coverage of unit tests
- graphql-download-local
- downloads external schema from a locally running BE
- graphql-download"
- downloads external schema from an external running BE
- graphql-codegen
- runs graphql codegen based on
graphql-codegen.yml
- runs graphql codegen based on
- storybook
- runs storybook locally
- test-storybook:coverage
- runs coverage on storybook tests
- build-storybook
- builds a snapshot of each story to be hosted somewhere, such as Chromatic
- start:staging
- starts the application using
nodemon
, compiles TypeScript and uses/db/prod.ts
data
- starts the application using
- test:e2e
- runs all Playwright tests
- test:e2e:watch
- runs Playwright tests in watch mode
When running npm run start
, create-react-app runs app in development
mode. In order to run app in production
mode run npm run build
then npx serve -s build
.
To ensure this app is connected to the correct env API add a .env.local
file for local / development mode and an .env.production.local
for production mode. For each file include REACT_APP_API_URL=<API-endpoint>
before serving app.
This app uses GitHub Actions as the CI/CD solution. There is an action for PR review that runs linting
, type checking
and tests
and there is an action to facilitate production deployment.
This app is deployed to Netlify. To set this up, a Github repository was created with existing committed code on main
branch. After logging in to Netlify this repository was imported from Github. The Github account was authorized with Netlify to allow the repository to be selected and then then the app was deployed via deploy site button on Netlify dashboard.
Once deployed, the Netlify site id
and personal access token
values were copied into the Github repo in secrets and variables
actions for both environment secrets
and repository secrets
as NETLIFY_AUTH_TOKEN
and NETLIFY_SITE_ID
.
The site id
was accessed by going to the site settings of this app that was deployed. The personal access token
was accessed by going to User settings > Applications > create a new personal access token
.
The REACT_APP_API_URL
environment variable was created in Netlify for this deployed project under site settings > environment variables
using the production value.
Since this app uses comment-on-commit
, the repo's GITHUB_TOKEN
permissions needs to be updated from default to read and write permissions
. This was achieved by logging into Github and going to ab-web-client > settings > actions > general
then updating the workflow permissions option
to read and write and saving.