0% found this document useful (0 votes)
28 views

LargeScaleWebApps - Copy - Copy (2)

Uploaded by

tutinhhao
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views

LargeScaleWebApps - Copy - Copy (2)

Uploaded by

tutinhhao
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 76

Large Scale Apps with React and TypeScript Build Large and Scalable front-

ends that leverage component isolation, a centralized state manager,


internationalization, localization, Custom Component Libraries, API-client
code that easily can switch between mocked data and live data and more.
Damiano Fusco This book is for sale at http://leanpub.com/react-typescript
This version was published on 2023-01-30 This is a Leanpub book. Leanpub
empowers authors and publishers with the Lean Publishing process. Lean
Publishing is the act of publishing an in-progress ebook using lightweight
tools and many iterations to get reader feedback, pivot until you have the
right book and build traction once you do. ©2022- 2023 Damiano Fusco
Contents LARGESCALEAPPSWITHREACTANDTYPESCRIPT. . . . . . . . . . . . . . . . . .
1 Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Goal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
TextConventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Thanks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Aboutme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
CompanionCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Chapter1-SettingUpTheProject. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
CreateProjectWizard. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Chapter1Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Chapter2-YourFirstComponent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
TheItemsList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
ItemsListComponentRequirements. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
ItemsListComponentCode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
MainAppView. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Chapter2Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Chapter3-DataModelsandInterfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
ModelsDirectory. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
InterfaceItemInterface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
ItemsListComponent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
AppView. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Chapter3Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Chapter4-AddingEventsTotheItemsComponent . . . . . . . . . . . . . . . . . . . . 29
ItemsListComponent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
CONTENTS
Chapter4Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Chapter5-IntrotoUnitTestingWhileRefactoringaBit . . . . . . . . . . . . . . . . . 38
ItemComponent. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
ItemComponentUnitTests. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
ItemsListcomponent. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Chapter5Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Chapter6-IntroducingStateManagement . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
StoreInterfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
StoreImplementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
App.tsx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Items.view.tsx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
BacktotheApp.tsxfile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
WebBrowser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
ItemsList.component.tsx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
BacktotheWebBrowser. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
LoaderComponent. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Chapter6Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Chapter7-ApiClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
APIClientOverview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Domains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
TheMainApiClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
ItemsdomainApiClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
MockandLiveApiClients. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
EnvironmentVariables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
ApiClientProvider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
StoreInstanceupdates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Alternatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Chapter7Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
Chapter8-EnhancetheApiClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
HttpClientInterfacesandModels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
UrlUtilsUnitTests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
HttpClient:UnitTests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
ItemsApiClientModelUpdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
Chapter8Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Chapter9-AppConfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
vite-env.d.tsupdates(orenv.d.ts) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
CONTENTS .envfilesupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . 132 ConfigInterface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
133 Configfiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
tsconfig.jsonupdates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
Configfilesmap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Configprovider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
UnitTests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
HttpClientcodeupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
ApiClientcodeupdates. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
Chapter9Recap. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Chapter10-LocalizationandInternationalization-LanguageLocalization . . . .
151 Plugins: i18next,react-i18next. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
151 Configupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
152 TranslationJSONdata. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
154 APIClientupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
156 UpdatestoApiClient.interface.ts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
159 UpdatestoApiClientinstances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
160 i18ninitializationanduseLocalizationhook. . . . . . . . . . . . . . . . . . . . . . . . .
162 main.tsxorindex.tsxupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
168 App.tsxupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
168 Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
170 Chapter10Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
175 Chapter 11- Localizationand Internationalization-Number andDateTime
Formatters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Directorylocalization/formatters. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Chapter11Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Chapter12-AddingTailwindCSS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
Chapter12Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Chapter13-IntrotoPrimitives. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
AtomicDesignandSimilarApproaches . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
GeneralStrategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
TextElements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
PrimitivesView. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
Chapter13Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Chapter14-MorePrimitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
CONTENTS
ButtonElements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
PrimitivesView-update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
Toggle/CheckboxElements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
PrimitivesView-onemoreupdate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
Chapter14Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Chapter15-APrimitiveModal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Icon:ElIconAlert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
InterfaceModalProps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
FileElModal.ts(note:not.tsx). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
FileuseModal.ts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
UpdatestoPrimitives.view.tsx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Chapter15Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Chapter16-Higher-levelcomponents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231 ItemComponent-updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
231 ItemsListComponent-updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
236 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
237 Chapter16Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
238 Chapter17-CreatingComponentLibraries . . . . . . . . . . . . . . . . . . . . . . . . . .
239 Createmy-component-library. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
239 Chapter17Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
248 Chapter18-CreatingaJavaScriptlibrary . . . . . . . . . . . . . . . . . . . . . . . . . . .
249 Createmy-js-helpers. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
249 Chapter18Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
258 Chapter19-PublishalibraryasaNPMpackage . . . . . . . . . . . . . . . . . . . . . . .
259 CreateanNPMuseraccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
259 CreateanOrganizationunderyourNPMprofile. . . . . . . . . . . . . . . . . . . . . .
259 Updatemy-js-helperspackage.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
260 Publishingthelibrary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
260 ConsumingyourNPMpackage. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
261 Chapter19Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
262 (MoreChaptersComingSoon) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
263 BonusChapter-usingcreate-react-
app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264 EnvVariables . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266 CONTENTS BonusChapter-Vitest. . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
RemoveJestdependenciesandsetupfiles . . . . . . . . . . . . . . . . . . . . . . . . . . 268
AddVitest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
UnitTestsupdates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
NamingConventions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
CodingStandards. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Websites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279
Blogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
Books . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
LARGE SCALE APPSWITHREACT ANDTYPESCRIPT This book is a guide for
developers looking to build large-scale front-end applications with React and
TypeScript. With the growth of the web and mobile app development, there is
an increasing demand for robust, scalable, and maintainable front-end
solutions. This book provides a comprehensive approach to building large
scale code bases that use React and TypeScript. The book covers key
concepts and best practices like: • Building front-ends that can grow to a
large code base that is organized and easy to expand and maintain. •
Development of UI components in isolation using an API client that can easily
serve live data or mocked data. • Centralized State Manager organized into
domain/area modules, providing a unified and consistent way to manage the
application state. • Internationalization and Localization for language
translation and number/dates formatting according to a specific culture,
making it easier to reach a global audience. • TypeScript type-checking at
development time to decrease run-time bugs or errors, reducing the risk of
costly bugs and enhancing the overall quality of the code. • Directory
structure, file, and code naming conventions, ensuring a consistent and
organized project structure for both developers and future maintainers. •
Hooks and Compositional Patterns, providing a flexible and reusable way to
imple ment functionality in components. • Components Libraries, allowing
developers to build a library of reusable components, reducing development
time and increasing code quality. • Unit tests for models and components,
ensuring code quality and reducing the risk of regressions. The book is
designed for developers with intermediate to advanced React and TypeScript
skills who are looking to take their applications to the next level. Whether
you are building a new large-scale app or optimizing an existing one, this
book will provide you with the tools and knowledge to achieve your goals.
Throughout the book, practical examples and real-world scenarios are used
to illustrate key concepts and best practices, providing you with a solid
understanding of how to build large scale apps with React and TypeScript.
LARGE SCALE APPS WITHREACTANDTYPESCRIPT 2 Copyright © 2022 by
Damiano Fusco (first published in January 2022) All rights reserved. No part
of this publication maybereproduced,distributed, or transmitted in any form
or by any means, including photocopying, recording, or other electronic or
me chanical methods, without the prior written permission of the author and
publisher, except in the case of brief quotations embodied in critical reviews
and certain other noncommercial uses permitted by copyright law. For
permission requests, write to the author and publisher, addressed “Attention:
Permissions Coordinator,” at the email [email protected]. Preface
WhyReact, Vite and what we mean by “large scale apps” in this book. React
is a popular JavaScript library for building user interfaces. It offers several
benefits for developers, such as: • Declarative code structure: React uses a
declarative syntax, making it easier for developers to understand how the UI
should react to changes in data. • Reusablecomponents:React’scomponent-
basedarchitectureallowsforbuildingreusable UI components, making it easier
to maintain and scale the codebase. • Virtual DOM: React uses a virtual
DOM, which optimizes updates and rendering, resulting in improved
performance compared to directly manipulating the actual DOM. • Server-
side rendering: React allows for server-side rendering, improving the initial
load time and making it easier to optimize search engine optimization (SEO).
• Large community: React has a large and active community, which means
developers have access to a wealth of resources, including tutorials, libraries,
and support. Vite is a modern build tool for JavaScript projects that aims to
provide fast and efficient builds. It offers several benefits, including: • Faster
build times and development experience compared to traditional bundlers. •
Lower initial load times, as only the essential code is loaded • Improved build
size, as Vite only includes the necessary code • Lightweight and optimized
for modern web development. When we refer to “large scale apps”, we mean
applications that have a large code base, a large number of users, and a
widerangeoffunctionality. These applications typically require efficient and
scalable code that can handle high traffic and large amounts of data. In this
kind of projects there are several common concerns that arise, such as: •
Code maintenance and scalability • Code quality and performance • Code
organization and structure Preface 4 To address these concerns, here we will
outline best practices for code organization and structure, such as using a
centralized state manager and implementing strong-type checking with
TypeScript. Additionally, we will focus on writing unit tests for our models and
components, which will help improve code quality and catch bugs early in
the development process. Our ultimate goal is to build a foundation that can
handle the demands of our app and be easy to expand and maintain as the
code base grows. Goal The primary aimof this book is to guide you through
the process of building a scalable React application by following best
practices for project structure, file organization, naming con ventions, state
management, type checking with TypeScript, and compositional approaches
using hooks. Throughout the chapters, we will grow our simple project into a
robust, large-scale appli cation that is easy to expand and maintain,
showcasing how patterns, conventions, and strategies can lay a solid
foundation and keep the code organized and uncluttered. We will build a
TypeScript API client that can seamlessly switch between serving static mock
data and communicating with a live API, allowing for front-end development
to commence even before the back-end API is fully functional. Additionally,
we will delve into topics such as internationalization, localization, and
advanced techniques, to round out our comprehensive guide to building a
scalable React application.
IMPORTANT:Wewillinitiallywritecodethatallowsustoachievethedesiredfunctiona
lity quickly, even if it requires more code, but then we constantly “rework” it
(refactoring) to improve it and find solutions that allow us to reduce the
amount of code used, or to organize it in a clear and easy way that is easy to
expand and maintain. So arm yourself with a lot of patience! Audience The
audience for this book is from beginners with some experience in MV*
applications, to intermediate developers. The format is similar to a cookbook,
but instead of individual recipes we’ll go through creating a project and keep
enhancing, refactoring, and make it better as we move forward to more
advanced chapters to show different patterns, architectures, and
technologies. Note: Some of the patterns illustrated here are not specific to
React¹, but can applied in any ¹Official website: https://reactjs.org Preface 5
application written in TypeScript or JavaScript. For example, most code from
Chapters 3, 7, 9 can also be used in Vue.js/Svelte/Angular or other front-end
frameworks, and even be used in NodeJS apps. Text Conventions I will
highlight most terms or names in bold, rather than define different
fonts/styles depending on whether a term is code, or a directory name or
something else. Thanks I would like to thank my son for helping me proof
read and validate the steps in each chapter by building the same project. I
also would like to thank all the developers that over the time helped me
correct things in the book and provided valuable feedback. About me I have
worked asasoftware developer for more than 20 years. I switched career
from being a full time musician when I was 30 years-old and then became a
graphic designer, then transi tion to a webdesigner wheninternet became “a
thing”, and for many years after that worked as full-stack developer using
Microsoft .NET, JavaScript, Node.js and many other technolo gies. You can
read more about me on my personal website https://www.damianofusco.com
and LinkedIn profile https://www.linkedin.com/in/damianofusco/. You will find
me also on Twitter, @damianome, and GitHub github.com/damianof
Prerequisites This
bookassumesthatyouarefamiliarwiththeterminal(commandpromptonWindows
), have already worked with the Node.js and NPM (Node Package Manager),
know how to install packages, and are familiar with the package.json file. It
also assumes you are familiar with JavaScript, HTML, CSS and in particular
with HTML DOMelements properties and events. It will also help if you have
some preliminary knowledge of TypeScript² as we won’t get into details
about the language itself or all of its features but mostly illustrate how to
enforce type checking at development time with it. You will need a text editor
like VS Code or Sublime Text, better if you have extensions/plu gins installed
that can help specifically for React/JSX code. For VS Code for example, you
could use extensions like react-vscode-extension-pack³ (just search for it
within the VS code extensions tab). ²https://www.typescriptlang.org
³jawandarajbir.react-vscode-extension-pack Companion Code
TheentirecompanioncodeforthebookcanbefoundonGitHubat:github.com/dami
anof/large scale-apps-my-react-project If you find any errors, or have
difficulty completing any of the steps described in the book, please report
themtomethroughtheGitHubissuessectionhere:github.com/damianof/large
scale-apps-my-react-project/issues You are also free to reach out to me
directly through Twitter at: @damianome Chapter 1- Setting Up The Project
IMPORTANT: This chapter assumes that you already have installed a recent
version of Node.js on your computer. If you do not have it yet, you can
download it here: https://nodejs.org/en/download/ There are manydifferent
waystocreate a Reactapp. Herewe’llbe leveraging TypeScript and therefore
will need to setup an project with a build/transpile process that will let us
make changes and verify them in real time. You could manually create this
project, install all the npmpackages required, create each individual file.
However, it is just much easier to do this by leveraging vite⁴ However, since
vite is still relatively new at the time of this book writing, it might be harder
for you to find help as your code-base grows larger, so this is something to
keep in mind. I added a bonus chapter at the end of the boook with
instructions on how to setup the project using create-react-app⁵ instead of
vite if you prefer that. Keep in mind that the use of environment variables
(which will cover in chapter 7 and 9), will differ. Create Project Wizard To set
up the project, use the terminal and execute the following node.js command:
npm init vite@latest If you do not have already installed the package create-
vite@latest⁶ it will prompt you to install it. In this case, type y and then enter
to proceed: Need to install the following packages: create-vite@latest Ok to
proceed? (y) The create-vite wizard will start and will ask you the name of
the project. The default is vite-project, so change this to my-react-project and
hit enter: ⁴https://vitejs.dev ⁵https://github.com/facebook/create-react-app
⁶https://www.npmjs.com/package/create-vite Chapter 1- Setting Up The
Project 9 ? Project name: › my-react-project The second step will ask to select
a framework. Use the keyboard arrows to scroll down the list and stop at
React, then hit enter: ? Select a framework: ›- Use arrow-keys. Return to
submit. Vanilla Vue React Preact Lit Svelte The third step will asking which
“variant” you want o use. Scroll down to TypeScript and hit enter: ? Select a
variant: ›- Use arrow-keys. Return to submit. JavaScript TypeScript This will
create a folder called my-react-project which is also the name of our project.
At the end it should display a message similar to this: Scaffolding project
in /Volumes/projects/my-react-project... Done. Now run: cd my-react-project
npm install npm run dev The first command will navigate to the current sub-
directory called my-react-project, the second one will install all the npm
dependencies, the third one will serve the app locally. You’ll see a message
similar to this displayed: VITE v3.1.4 ready in 303 ms Local:
http://127.0.0.1:5173/ Network: use--host to expose Chapter 1- Setting Up
The Project 10
Fromthewebbrowser,navigatetothehttp://localhost:5173addressandyou’llseea
pplication home page rendered: The my-react-project has been created with
one main view called App.tsx. Chapter 1- Setting Up The Project 11 Chapter 1
Recap WhatWeLearned Howto create the basic plumbing for a React app
using the vite and create-vite@latest • Howto serve the app locally through
the command npm run dev Observations • The app has been created with
only preliminary code • The app does not do much yet, has only the main
App view with some static html in it Based on these observations, there are a
few improvements that will be making into the next chapter: Improvements •
Expand our app functionality by creating our first component Chapter 2- Your
First Component The Items List Let’s now pretend we have been giving
requirements for our app to have a component that displays a list of ”items”.
We will keep this simple initially and as we move towards more advanced
chapter expand on it to show how we can better structure our application to
support: • Quick prototyping and development using mocked data •
Component Organization • Unit Testing • State Management •
Internationalization support so we can render our user interface using
different lan guages • Localization for number and date formatting for
different cultures ItemsList Component Requirements Your initial version of
the ItemsList component, will have to implement the following requirements
(later, in more advanced chapters, we will expand on these as we get into
more advanced topics): • The component will display a list of items • Anitem
will have 3 properties: * id * name * selected • The item name will be
displayed to the user • The user should be able to select/deselect one or
more item • Anicon will be shown next to the name to indicate if the item is
selected Chapter 2- Your First Component 13 ItemsList Component Code
Within the src/componentsdirectory, create a sub-directory called items.
Within this folder add a new file called ItemsList.component.tsx⁷ Your
directory structure will now look like this: Within the ItemsList.component.tsx
file, paste the following code: // example using const of type React.FC: import
React from 'react' export const ItemsListComponent: React.FC = (props) =>
{ return (

Items:
⁷We are following a file naming convention where higher level components’
names are pascal-case and follow this format [Component
Name].component.tsx- Reference: Naming Conventions section at the end of
this book Chapter 2- Your First Component 14 { props.items.map((item,
index) =>

 {item.name}

)}

) } Afewthings to notice here. There are different ways to create a React


component. You could just return a function, a class, or like in the example
above, a const of type React.FC⁸ NOTE:Deciding whether to use afunction or
aclass might be amatter of personal preference, or just abiding the coding
standard you have defined with your team in your organization. If you google
React.FC vs class you’ll get several blogs/articles where it seems the majority
of developers prefer pure function or classes, rather than React.FC. Going
forward, I’ll try to use classes or functions throughout the book and avoid
React.FC (but there might be cases where I use any of the three) When using
a const of type React.FC, you will need to return the component html
wrapped with parentheses. If using a class, you will need to implement a
render function that returns the html. For example, using the class syntax,
the above component can be re-written as: // example using class extending
component import React from 'react' export class ItemsListComponent
extends React.Component { constructor(props: { items: any[] })
{ super(props) } render(): React.ReactNode { const { items } = this.props
return

Items:

⁸React.FC Chapter 2- Your First Component 15

{ items.map((item: any, index: number) =>

 {item.name}

)}

} } Please go ahead and replace the code with the above using the class
syntax. Then save and verify everything still renders as before without error
in the browser console. Here is what we are doing int he component code
above: For our html, we are returning a

element containing: • a
element with hard-coded text just saying ”Items:” • a

element with some code that will render all our items as

 elements. Weusethe JavaScript Array native map method to


loop through the Items array and return an

 element for each item in the array. The

 element will display the item name in the browser. Note how
we have to also specify the key attribute which is required to
be unique within a list rendered by React. Here we leverage
the fact the the map method returns the index of the item in
the array as the second argument to our handler function
(index). The index is good enough for now to use for the key
attribute.
Notethatwithmapyoucaneitherinlinethereturnexpression,thusn
otneedingthekeyword return: items.map((item, index) =>

 {item.name}

) Or you could use {} (curly braces) for the function body, and use
the return keyword in this case: items.map((item, index) =>
{ return

 {item.name}

}) Whichsyntax youuse is up toyourpreference. However, remember


that in a team, especialy in alarge organization, there will be coding
standards that will dictacte how you consistently write code. You
should always abide the standard that
youandyourteamhaveagreeedupon. Note also that wedeclared
theitems property as an array of any⁹for now(later we’ll replace
⁹With ‘any’, TypeScript does not enforce type-checking on a
property or variable. However, this is considered a bad practice as
we lose the main benefit of TypeScript. There might be exceptions
to this rule when using older 3rd party packages/libraries/plugins
that do not offer type definitions. However, even in those cases it
would be strongly recommended to provide interfaces and types so
that you can still avoid using ‘any’. Chapter 2- Your First Component
16 any with an interface we’ll create): ... { items: any[] // avoid
using "any", in later chapters we'll replace with a TS int\ erface } ...
Main AppView Open the src/App.tsx file. Replace the entire existing
code with this: //file: src/App.tsx // component: function App()
{ return (

...

); } export default App Let’s start by adding at the top an import to


reference our ItemsList.component.tsx: //file: src/App.tsx // import
reference to your ItemsList component: import
{ ItemsListComponent } from
'./components/items/ItemsList.component' ... For now, quickly mock
some data for our list of items that we will feed to our ItemsList
Component. For this we instantiate a local variable called items and
initialize it with some hard-coded data¹⁰. Wedothis before the App
function declaration: ¹⁰Note: using hard-coded data is a bad practice
and here we are only doing it to first illustrate how things flow, and
later in the next chapters will remove in favor of best practices and
patterns (see Chapter 5) Chapter 2- Your First Component 17 //file:
src/App.tsx // import reference to your ItemsList component: import
{ ItemsListComponent } from
'./components/items/ItemsList.component' // mock data: const
items: any[] = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }] // component: function App() { ... Finally,
we modify the content inside the

(within the return statement). Let just add our ItemsListComponent


and pass the hard-coded items data to it through its property items.
The complete code within the App.tsx file should now look like
this: //file: src/App.tsx // import reference to your ItemsList
component: import { ItemsListComponent } from
'./components/items/ItemsList.component' // mock data: const
items: any[] = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }] Chapter 2- Your First Component 18 //
component: function App() { return (

); } export default App Update src/main.tsx by removing or


commenting the imported index.css file: import React from 'react'
import ReactDOM from 'react-dom/client' import App from './App' //
import './index.css' // <-- comment out or remove this line
ReactDOM.createRoot(document.getElementById('root') as
HTMLElement).render( ) Save the file. The web browser will refresh
and display our preliminary items list being rendered more or less
like this: Chapter 2- Your First Component 19 Chapter 2- Your First
Component 20 Chapter 2 Recap WhatWeLearned • Howto create a
basic component that displays a list of items • Howto consume that
component from another component or view Observations •
Theitemsproperty within the ItemsList.component.tsx is declared as
an array of type any • The App.tsx view contains hard-coded data
(items) which is also declared as an array of any • This means we
are not leveraging strong-type checking at development time using
TypeScript interfaces/models/types Based on these observations,
there are a few improvements that we will make in the next
chapters: Improvements • Create a TypeScript interface called
ItemInterface for enforcing type checking at development time for
our items data • Update our code so it uses the new ItemInterface
interface Chapter 3- Data Models and Interfaces In this chapter, we
will delve into the power of TypeScript by leveraging its strong-type
checking capabilities through the use of interfaces. One of the main
challenges with pure JavaScript is its loosely typed nature, which
can lead to unexpected behavior and bugs at run-time. This is due
to the lack of checks on the type or properties of values or objects
being passed around in the code. TypeScript solves this problem by
providing developers with the ability to enforce strict type checking
at development time through the use of interfaces, types, classes,
and more. Byincorporating TypeScript into our project, we’ll be able
to catch potential issues and bugs before they reach the production
environment, saving us time and resources in debugging and fixing.
Moreover, strong-typing also improves the readability and
maintainability of our code, making it easier for developers to
understand the purpose and usage of values and objects in the
codebase. In this chapter, we’ll explore how to use interfaces and
types to implement strong-typing in our project, and how it can help
us ensure the reliability and quality of our code. Models Directory To
lay the foundation for building large-scale applications, we will start
by creating a new sub-directory under src called models. The
organization of files and directories plays a critical role in the
success of large-scale code bases. As such, it’s essential to
establish a consistent naming convention and directory structure
from the outset. This will help ensure that the code remains
organized, easy to understand, and maintainable as the application
grows and the number of source files and directories increases. You
andyourteamarefree to determine the standards that work best for
you, but it’s crucial to establish a set of conventions and stick to
them. This will save you a significant amount of time and effort in
the long run and prevent confusion and headaches as the
application grows and evolves. Chapter 3- Data Models and
Interfaces 22 Interface ItemInterface To create the interface for our
items, we will create a new directory called src/models/items and
add a TypeScript file named Item.interface.ts. It’s worth noting that
there are different naming conventions for TypeScript interfaces,
with somepreferring to use a suffix like Interface, while others use a
prefix like I. In this book, we will follow the suffix convention, using
Item.interface.ts as the file name. However, you are free to choose
your preferred naming convention or align with your organization’s
coding standards. It’s important to keep each interface in its own
file, as this makes it easier to maintain and manage. For more
information on naming conventions, please refer to the Naming
Conventions section at the end of this book. Your directory structure
should now look similar to this: Let’s write an interface that
represents one item that will be rendered in ourItem component.
Our interface will have three properties: • id: this is a unique
number for each item in the list • name: is a string containing the
name of the item • selected: is a boolean value that shows if the
user has selected the item The code for your interface should look
like this: Chapter 3- Data Models and Interfaces 23 // file:
src/models/items/Item.interface.ts export interface ItemInterface
{ id: number name: string selected: boolean } For now, that is all we
need. Since this will only represent a piece of data, we do not need
to implement a class. NOTE: In this case our ItemInterface only
holds fields, but no methods. You can think of this more like the
type struct in language like C or C#. Unfortunately TypeScript does
not have an explicit struct type¹¹ and their guidance is to use
interfaces for this. ItemsList Component Nowthat wehave our
interface, we can finally leverage TypeScript type checking ability by
changing our items property on the items component from any[] to
ItemInterface[]. First, import a reference for ItemInterface: // file:
src/components/items/ItemsList.component.tsx import React from
'react' // import reference to our interface import { ItemInterface }
from '../../models/items/Item.interface' ... Then modify our items
property declaration from any[] to ItemInterface[]: ¹¹There have
been suggestions presented, but I do not think they will ever add a
struct type. See the TypeScript team answers here:
https://github.com/microsoft/TypeScript/issues/22101 Chapter 3-
Data Models and Interfaces 24 // file:
src/components/items/ItemsList.component.tsx // example using
const of type React.FC: import React from 'react' // import reference
to our interface import { ItemInterface } from
'../../models/items/Item.interface' export const ItemsListComponent:
React.FC = (props) => { ... The complete update code should look
like this: // file: src/components/items/ItemsList.component.tsx //
example using const of type React.FC: import React from 'react' //
import reference to our interface import { ItemInterface } from
'../../models/items/Item.interface' export const ItemsListComponent:
React.FC = (props) => { return (

Items:

{ props.items.map((item, index) =>

o {item.name}

)}

) } Or if you went with the class syntax: Chapter 3- Data Models and
Interfaces 25 // file: src/components/items/ItemsList.with-class-
syntax.tsx // example using class extending component import React
from 'react' // import reference to our interface import
{ ItemInterface } from '../../models/items/Item.interface' export
class ItemsListComponent extends React.Component
{ constructor(props: { items: ItemInterface[] // replace any[] with
ItemInterface[] }) { super(props) } render(): React.ReactNode
{ const { items } = this.props return

Items:

{ items.map((item: any, index: number) =>

o {item.name}

)}

} } Make sure the terminal does not display any error, and that the
web browser refreshed and no error are displayed in the browser
console. AppView Weshould also update the App.tsx code so it uses
the ItemInterface interface for the locally private property also
called items. Please note, that as soon as you changethe
itemsproperty fromany[]toItemInterface[] it will complain that
eachitemdoesnotcorrectly implementthe interface. This is because
we did not initially include the selected property required by the
interface. This is one of the powerful Chapter 3- Data Models and
Interfaces 26 things of using TypeScript correctly. It will help catch
errors like this at development time rather than run time, increase
the code quality and make it less prone to bugs. So make sure each
item has now also a selected field with a default of false. // file:
src/App.tsx import './App.css' // import reference to our interface
import { ItemInterface } from './models/items/Item.interface' //
import reference to your ItemsList component: import
{ ItemsListComponent } from
'./components/items/ItemsList.component' // mock data: const
items: ItemInterface[] = [{ // change any[] to ItemInterface[] id: 1,
name: 'Item 1', selected: false // add selected: false to each item },
{ id: 2, name: 'Item 2', selected: false }, { id: 3, name: 'Item 3',
selected: false }] ... Again, make sure the terminal does not display
any errors, and that the web browser refreshed and no error are
displayed in the browser console. As you make changes is also a
good idea occasionally to do an Empty Cache and Hard Reload by
right clicking on the Chrome refresh icon and selecting the last
option: Chapter 3- Data Models and Interfaces 27 Chapter 3- Data
Models and Interfaces 28 Chapter 3 Recap WhatWeLearned • It’s
important to follow files and directories naming convention and
structure conven tion • How to leverage TypeScript interfaces and
avoid using any so that strong-type checking is enforced at
development time and avoiding potential runtime errors or hidden
bugs Observations • The App.tsx contains a local variable that holds
hard-coded mocked data that enabled us to prototype our
component quickly • ItemsList.component.tsx just displays the list
of items, but the user has still no ability to click on them to change
their selected property Based on these observations, there are a
few improvements that we will make into the next chapter:
Improvements • Update our component so that when a user clicks
on an item displayed on the page, the item selected property will
toggle from false to true (and vice versa) Chapter 4- Adding Events
To the Items Component In this chapter we keep building our
ItemsList.component.tsx so we can handle when the user clicks on
an item in the list. ItemsList Component Start by adding a function
called handleItemClick just before the render() function. This
function will handle a click on each of the
 elements and will toggle the item.selected property from true
to false or vice versa. It will also logs the item id and selected
properties to the console for preliminary debugging: // file:
ItemsList.component.tsx ... // if using class syntax
handleItemClick (item: ItemInterface) { item.selected = !
item.selected console.log('handleItemClick', item.id,
item.selected) } render() { ... // or if using React.FC syntax:
const handleItemClick = (item: ItemInterface) =>
{ item.selected = !item.selected console.log('handleItemClick',
item.id, item.selected) } return ( ... Then update the
return/render section of the render() function by adding an
onClick attribute to the

 element, pointing to an handler called handleItemClick and


passing a reference to item as the argument: Chapter 4-
Adding Events To the Items Component 30 // file:
ItemsList.component.tsx ... // if using class syntax: render():
React.ReactNode { const { items } = this.props return (

Items:

{ items.map((item: any, index: number) =>

o t\ his.handleItemClick(item)}>{item.name}

)}

) } ... // or if using React.FC syntax: ... return (

Items:

{ props.items.map((item, index) =>

o handleItem\ Click(item)}>{item.name}

)}

) ... Note that React uses its own syntax for html attributes
(because of JSX), and the standard html onclick event is called
onClick (note the letter casing) in React. Additionally, the onClick
attribute expect a method with a specific signature, and we should
add wrap it within an inline funciton in this (or TypeSCript will throw
an error): Chapter 4- Adding Events To the Items Component 31 ()
=> handleItemClick(item) Save the file. The web browser should
have refreshed, and when clicking on the items in the list you
should see the message being displayed in the browser developer
console, and whenclicking multiple time on the same item it should
print true then false etc showing that toggling is working: Now, we
learned how to add a click handler to our component and changing
the data item selected property that way. However, updating the
selected property within the onItemSelect will not cause React to
re-render the html. This is because the data we are working with is
not yet reactive. Let’s verify this. Start by slightly modifying the
text output by our list element, outputting also the selected value
within [] (square brackets) like “[]”: // file:
ItemsList.component.tsx ... props.items.map((item, index) =>
{ return (

 handleItemClick(item)}> {item.name} [{ String(item.selected)


}] {/* output item.selected n\ ext to the name */}

) }) ... Note: React is peculiar when rendering some types of


properties. If you were trying to just render item.selected, which is
a boolean, without either wrapping with String() or call
item.selected.toString(), then it will never render its value. Save
and check the browser again. Notice that even though after clicking
on the list items you see a message in the console with the updated
value for the selected property, the html is not being re-rendered.
Chapter 4- Adding Events To the Items Component 32 In order to
make our data reactive, we have to use React’s hook useState. Let’s
try this. First, lets modify the code in our ItemsListComponent so
that it takes also a second property called onItemSelect. While we
add onItemSelect property, let’s also refactor a bit and create a
type/interface for our component properties called simply Props.
There is no need for a better name as we’ll not be exporting this
type/interface but using it only within the ItemsList.component.tsx
code: // file: ItemsList.component.tsx // if using the class
syntax: ... // extract type for component properties: type Props =
{ items: ItemInterface[], onItemSelect: (item: ItemInterface) =>
void } // remove the type declaration within React.Component<>
and replace it with Props. // also, change the constructor signature
to use Props as the type of the props argu\ ment: export class
ItemsListComponent extends React.Component { constructor(props:
Props) { super(props) } ... // or if using React.FC syntax: ... // change
the function type signature to use Props as the type of the props
argument: export const ItemsListComponent: React.FC = (props) =>
{ ... // NOTE: React is perfectly happy with normal function
signatures so you could simpl\ y use this if you prefer: export const
ItemsListComponent = (props: Props) => { ... ... Then, modify the
function onClick to just invoke the props.onItemSelect functoin that
is passed by the parent component: Chapter 4- Adding Events To the
Items Component 33 // file: ItemsList.component.tsx ...
handleItemClick (item: ItemInterface)
{ this.props.onItemSelect(item) // Note: you need to use the "this"
prefix here only\ if using class syntax } ... Now, openthe App.tsx file
and lets modify some code in here. Start by importing a reference at
the top to the hook useState: import { useState } from 'react' Then
remove our mock data array. Instead, within the function App(), add
this code: // file: App.tsx ... // begin: remove code block // mock data:
// const items: ItemInterface[] = [{ // id: 1, // name: 'Item 1', //
selected: false // }, { // id: 2, // name: 'Item 2', // selected: false // },
{ // id: 3, // name: 'Item 3', // selected: false // }] // end: remove code
block function App() { // begin: add code block // add the useState
declaration here passing our mock-data array as an argument const
[items, setItems] = useState([{ id: 1, name: 'Item 1', selected:
true }, { Chapter 4- Adding Events To the Items Component 34 id: 2,
name: 'Item 2', selected: false }, { id: 3, name: 'Item 3', selected:
false }]) // end: add code block ...
WhatwearedoinghereisinvokethehookuseState,specifyingthetypetob
eItemInterface[], and pass our initial mock data array in it. The
useState hook returns an array where the first parameter is a
reference to your data, in this case items, the second parameter is a
funciton that allow to update the data, in this case we called it
setState. Nowlet’s add a function called onItemSelect. Since we are
already within a function (App), we can either declare is as a arrow
functoin stored into a const, or as a pure function: // file:
App.tsx ... // either as: function onItemSelect (item: ItemInterface) {
... // or as: const onItemSelect = (item: ItemInterface) => { .... Let’s
go with an arrow function. Here is the full code for the function: //
file: App.tsx ... // begin: add code block const onItemSelect = (item:
ItemInterface) => { const updatedItems = [...items] const found =
updatedItems.find(o => o.id === item.id) as ItemInterface
found.selected = !item.selected setItems(updatedItems)
console.log('App.tsx: onItemSelect', found.id, found.selected,
updatedItems) } // end: add code block Chapter 4- Adding Events To
the Items Component 35 ... Finally, modify the return() section to
pass our onItemSelect handler function through a property with the
same on ItemsListComponent: // file: App.tsx ... return (

) Here is the full update code of App.tsx: // file: src/App.tsx import


{ useState } from 'react' // import reference to our interface import
{ ItemInterface } from './models/items/Item.interface' // import
reference to your ItemsList component: import
{ ItemsListComponent } from
'./components/items/ItemsList.component' // component: function
App() { // add the useState declaration here passing our mock-data
array as an argument const [items, setItems] = useState([{ id: 1,
name: 'Item 1', selected: true }, { id: 2, name: 'Item 2', selected:
false }, { id: 3, name: 'Item 3', selected: false }]) const
onItemSelect = (item: ItemInterface) => { const updatedItems =
[...items] Chapter 4- Adding Events To the Items Component 36
const found = updatedItems.find(o => o.id === item.id) as
ItemInterface found.selected = !item.selected
setItems(updatedItems) console.log('App.tsx: onItemSelect',
found.id, found.selected, updatedItems) } return (

); } export default App Save the file, and check the web browser.
This time, you can see the html re-rendering and the correct value,
either true/false, displayed next to each item as you click on them.
Chapter 4- Adding Events To the Items Component 37 Chapter 4
Recap WhatWeLearned • Howto add a click handler to our ItemsList
component • Howto manipulate the item.selected property through
our click handler • How to use the React hook useState to create a
reactive property named items, and a method to update the React
state Observations • The items selected property is being
manipulated directly within our component • We need a more
centralized way to handle changes on the data and state of the
application Based on these observations, there are a few
improvements that we will make in the next chapters:
Improvements • Implement a state manager to control our
application state from a centralized place Chapter 5- Intro to Unit
Testing While Refactoring a Bit Wewill now delve into writing unit
tests for our project. Unit tests serve as a critical aspect of ensuring
the stability and reliability of our code. In this book, we will cover
two main categories of unit tests: • Unit tests for models, classes,
structures, and interfaces (such as the API client and helpers) • Unit
tests for React components Note: It’s worth mentioning that there is
a third category of tests, known as end-to-end (e2e) tests, but we
will not be covering those in this book. Our first step will be to write
unit tests for our React components. We will start with the ItemsList
component and while doing so, we will make some refactors to
improve its implementation. The unit tests will validate the changes
we make, ensuring that our code remains functional and free of
bugs. ItemComponent Remember how in our ItemsList component
we have a loop that creates

 elements, one for each item in our items property? Let’s


extract the code for the

 element and create a child component just for that. Let’s start
by adding a new file called Item.component.tsx under the
src/components/items/children directory: Chapter 5- Intro to
Unit Testing While Refactoring a Bit 39 Paste the following
code in the Item.component.tsx file: // file: Item.component.tsx
import React from 'react' // import reference to our interface
import { ItemInterface } from
'../../../models/items/Item.interface' // component props type:
type Props = { testid: string model: ItemInterface,
onItemSelect: (item: ItemInterface) => void } // example using
class syntax export class ItemComponent extends
React.Component { constructor(props: Props) { Chapter 5-
Intro to Unit Testing While Refactoring a Bit 40 super(props) }
get cssClass () { let css = 'item' if (this.props.model?.selected)
{ css += ' selected' } return css.trim() } handleItemClick (item:
ItemInterface) { this.props.onItemSelect(item) } render():
React.ReactNode { const { model } = this.props const testid =
this.props.testid || 'not-set' return (

 this.handleI\ temClick(model)}>

{model.name}

) } } Note: we added also a testid property that will bind to the


data-testid property of the outer html DOM element of our
component. This will make it easier to select the element during the
unit tests or automation tests. Wejust created a template for a
single
 element. We also enhanced this a bit by replacing the
rendering of the name with binding { item.name } with two
child

elements:- one to display the Item name- one that will show a star
icon (we are just using a char here, but in the next chapters we’ll be
replacing this with real icons from the font library material-icons)
Then we added a computed property called cssClass that will return
the string ”item” or ”item selected”. We then bind this to the

 className attribute, based on whether the model.selected


property is true or false:

 Chapter 5- Intro to Unit Testing While Refactoring a Bit 41 This


will have the effect to render the

 element in two possible ways:-

 (when not selected)-

 (when selected) We also bind to the click event with onClick


binding and in the local onClick handler we just invoke the
parent handler through the prop onItemSelect and pass it the
model as the argument (props.model). We will then handle this
in the parent component (ItemsList component as before).
App.css Let’s also replace the content of the file App.css with
this quick-and-dirty css: /* file: App.css */ .App { padding:
20px; } ul { padding-inline-start: 0; margin-block-start: 0;
margin-block-end: 0; margin-inline-start: 0px; margin-inline-
end: 0px; padding-inline-start: 0px; } li.item { padding: 5px;
outline: solid 1px #eee; display: flex; align-items: center;
height: 30px; cursor: pointer; transition: background-color 0.3s
ease; } li.item .name { margin-left: 6px; } li.item .selected-
indicator { font-size: 2em; line-height: 0.5em; margin: 10px
8px 0 8px; color: lightgray; Chapter 5- Intro to Unit Testing
While Refactoring a Bit 42 } li.item.selected .selected-indicator
{ color: skyblue; } li.item:hover { background-color: #eee; }
Note: the css above is just a quick-and-dirty bit of styling so
we can make our list look a bit prettier for now. In later
chapters we’ll introduce TailwindCSS and keep working with
that instead of writing our own css. Within the src/App.tsx you
went to restore/uncomment the line we commented out or
removed in earlier chapters: // file: src/App.tsx import
'./App.css' // <-- restore this import ... Install npm
dependencies for unit tests Let’s install Vitest and other npm
libraries we need to be able to run the unit tests: npm i-D
vitest jsdom @testing-library/react @testing-library/user-event
@types/jest Configuration Nowweneed to configure a few
things to be able to run unit tests. tsconfig.json file Add
“vite/client” and “vitest/globals” to tsconfig.json
compilerOptions types: Chapter 5- Intro to Unit Testing While
Refactoring a Bit 43 // file: my-react-project/tsconfig.json ...
"compilerOptions": { ... "baseUrl": ".", "paths": { "@/*": [ "src/*"
] }, "types": [ "react", "vite/client", "vitest/globals" ] ...
vite.config.js files Add “test” section with the following
settings to the vite.config.js files: // file:
my-react-project/vite.config.js (and any other vite.config.xyz.js
file) /// /// import { defineConfig } from 'vite' import react from
'@vitejs/plugin-react' // https://vitejs.dev/config/ export default
defineConfig({ plugins: [ react() ], test: { globals: true,
environment: 'jsdom', exclude: [ 'node_modules' ] } }) Chapter
5- Intro to Unit Testing While Refactoring a Bit 44 package.json
Within the package.json file, add the following command
shortcuts within the script section: ... "scripts": { ... "test":
"vitest run", "test-watch": "npm run test----watch" } ...
ItemComponent Unit Tests Now, let’s add our first unit tests for
our newly created component. Within the same directory
where our Item.component.tsx is located, add two new files: •
one called Item.rendering.test.tsx • one called
Item.behavior.test.tsx Your directory structure will look now
ike this: Chapter 5- Intro to Unit Testing While Refactoring a Bit
45 NOTE: Jest has become quite old at this point and hard to
work with epecially in Vite as it requires a lot of dependencies
and setup. I strongly suggest to use Vitest¹² and added a
bonus chapter at the end of the book with instructions on how
to do this. Furthermore, additional unit tests will add in the
more advanced chapters will be using Vitest.
Item.rendering.test.tsx Open the file Item.rendering.test.tsx
and paste the following code in it: ¹²https://vitest.dev Chapter
5- Intro to Unit Testing While Refactoring a Bit 46 // file:
Item.rendering.test.tsx import { render, screen, prettyDOM }
from '@testing-library/react' // import reference to our
interface import { ItemInterface } from
'../../../models/items/Item.interface' // import reference to your
Item component: import { ItemComponent } from
'./Item.component' describe('Item.component: rendering' , ()
=> { it('renders an Item text correctly', () => { const testid =
'unit-test-item' const model: ItemInterface = { id: 1, name:
'Unit test item 1', selected: false } // render component render(
{}} />) // get element reference by testid const liElement =
screen.getByTestId(testid) // test
expect(liElement).not.toBeNull() // get element children const
children = liElement.children expect(children).toHaveLength(2)
expect(children.item(1)?.innerHTML).toContain('Unit test item
1') }) it('renders an Item indicator correctly', () => { const
testid = 'unit-test-item' const model: ItemInterface = { id: 1,
name: 'Unit test item 2', selected: false } // render component
render( {}} />) Chapter 5- Intro to Unit Testing While
Refactoring a Bit 47 // get element reference by testid const
liElement = screen.getByTestId(testid) // test
expect(liElement).not.toBeNull() // get element children const
children = liElement.children expect(children).toHaveLength(2)
expect(children.item(0)?.innerHTML).toEqual('*') }) // we'll add
more here in a second }) ... Note: we are leveraging here the
React testing-library¹³, make sure you install the necessary
dependencies (see the repository for the book code on
GitHub). Wetest that the component renders the data model
properties as expected. For now, we are checking if the entire
text rendered by the component contains the model.name and
also that there is an element rendering the *. This is not very
precise as our component later might render additional labels
and our test might match these instead resulting in possible
false positives. Note: These example are just to get you
started. Later you can look at more precise ways to test what
our component has rendered or even trigger events on them.
Run our unit tests from the terminal with this command: npm
run test It should run the unit tests and print the results on
the terminal, similar to this:
¹³https://testing-library.com/docs/react-testing-library/intro
Chapter 5- Intro to Unit Testing While Refactoring a Bit 48 >
vitest run RUN v0.23.4 /my-react-project/ ✓
src/components/items/children/Item.rendering.test.tsx (2) Test
Files 1 passed (1) Tests 2 passed (2) Start at 08:45:41 Duration
829ms (transform 269ms, setup 0ms, collect 132ms, tests
15ms) Let’s add two more tests within the same file to check
that the component has the expected CSS classes. Test to
check that it has the class ”selected” when item.selected is
true, and that does NOT have the css class ”selected” when
item.selected is false: // file: Item.rendering.test.tsx ...
describe('Item.component: rendering' , () => { ... it('has
expected css class when selected is true', () => { const testid
= 'unit-test-item' const model: ItemInterface = { id: 1, name:
'Unit test item 3', selected: true } // render component
render( {}} />) // get element reference by testid const
liElement = screen.getByTestId(testid) // test
expect(liElement).not.toBeNull() // check that the element
class attribute has the expected value
expect(liElement.className).toContain('selected') Chapter 5-
Intro to Unit Testing While Refactoring a Bit 49 }) it('has
expected css class when selected is false', () => { const testid
= 'unit-test-item' const model: ItemInterface = { id: 1, name:
'Unit test item 3', selected: false } // render component render(
{}} />) // get element reference by testid const liElement =
screen.getByTestId(testid) // test
expect(liElement).not.toBeNull() // check that the element
class attribute does not contain 'selected'
expect(liElement.className).not.toContain('selected') }) })
Item.behavior.test.tsx Wecanalsotest the behavior of our
component by programmatifcally triggering the onClick event.
Let’s open the file Item.behavior.test.tsx and paste the
following code in it: // file: Item.behavior.test.tsx import
{ render, fireEvent, prettyDOM } from '@testing-library/react' //
import reference to our interface import { ItemInterface } from
'../../../models/items/Item.interface' // import reference to your
Item component: import { ItemComponent } from
'./Item.component' describe('Item.component: behavior' , () =>
{ // test our component click event it('click event invokes
onItemSelect handler as expected', () => { Chapter 5- Intro to
Unit Testing While Refactoring a Bit 50 const model:
ItemInterface = { id: 1, name: 'Unit test item 1', selected: false
} // create a spy function with vitest.fn() const onItemSelect =
vitest.fn() const testid = 'unit-test-item' // render our
component const { container } = render() // get a reference to
the

 element const liElement = container.firstChild as HTMLElement


// fire click fireEvent.click(liElement) // check test result
expect(onItemSelect).toHaveBeenCalledTimes(1) }) }) Save
and check the test results and make sure all pass (if you had
stopped it, run npm run test again). ItemsList component Now
we can finally modify our ItemsList.component.tsx to consume
our newly created Item component. Import a reference to
ItemComponent, then replace the return section within the
items.map to use our component instead of the

 element: Chapter 5- Intro to Unit Testing While Refactoring a


Bit 51 // file: ItemsList.component.tsx import React from 'react'
// import reference to our interface import { ItemInterface }
from '../../models/items/Item.interface' // import reference to
your Item component: import { ItemComponent } from
'./children/Item.component' // if using class syntax: type Props
= { items: ItemInterface[], onItemSelect: (item: ItemInterface)
=> void } export class ItemsListComponent extends
React.Component { constructor(props: Props) { super(props) }
handleItemClick (item: ItemInterface)
{ this.props.onItemSelect(item) } render(): React.ReactNode {
const { items } = this.props return (

Items:

{ items.map((item: any, index: number) => { // remove this line: //


return

o this.handleItemClick(item)}>{\ item.name}

// replace with this line that replaces

o with : return this.handleItemClick(item)}> }) }

) Chapter 5- Intro to Unit Testing While Refactoring a Bit 52 } } // if


using function syntax: type Props = { items: ItemInterface[],
onItemSelect: (item: ItemInterface) => void } export const
ItemsListComponent: React.FC = (props) => { const handleItemClick
= (item: ItemInterface) => { props.onItemSelect(item) } return (

Items:
{ props.items.map((item, index) => { // remove this return block: //
return ( //

o handleItemClick(item)}> // {item.name}
[{ String(item.selected) }] {/* output item.selecte\ d next
to the name */} //

// ) // add this return block: return ( handleItemClick(item)}> ) }) }

) } In the web browser, the list should now render similar to this
(here we are showing it after we clicked on the 2nd item element
and is now selected): Chapter 5- Intro to Unit Testing While
Refactoring a Bit 53 Chapter 5- Intro to Unit Testing While
Refactoring a Bit 54 Chapter 5 Recap WhatWeLearned • Howto write
unit tests against a component • How to test that components
render specific DOM elements, or have specific text, or attributes
like CSS classes, etc. • How to test events on our components by
programmatically triggering them with fireEvent (from React Testing
Library) • How to re-factor parts of a component to create a child
component and use unit tests to validate our changes Observations
• Wedid not test our ItemsList.component.tsx or more advanced
behaviors Based on these observations, there are a few
improvements that you could make: Improvements • Add additional
unit tests for ItemsList.component.tsx as well Chapter 6-
Introducing State Management One of the most important part of an
app that will grow large is to decided how to manage its state. For
many years in MV* frameworks like React¹⁴ or Vue¹⁵ etc. that meant
using a state manager that usually implemented the Flux¹⁶ State
Management pattern. With React that usually meant using Redux¹⁷,
while with Vue it meant using Vuex¹⁸, even though nowadays there
are other alternatives (inluding building your own custom state In
Vue using just Vue reactive¹⁹, or the useState²⁰ hooks in React, etc).
Flux offers an architectural pattern that is a slight modification of
the observer-observable pattern and it is not a library or a
framework. Single source of truth: The most important reason to
implement a centralized state manager is to have a “single source
of truth” for the application state/data. This simply means that our
application state has only one global, centralized source. The
responsibility of changing that state is only in the hand of our state
manager. That means you can expect a consistent behavior in your
app as the source of your data cannot be changed outside the state
manager. Unfortunately, Redux comes with a learning curve,
complexity, and during the years alternatives have come up that
offer simpler ways to manage the app state. There has been also a
lot of debate where is it really worth it for most small to medium
apps. I’d argue that is most likely a valid choice for large scale apps.
However, It is outside the scope of this book to tell you which
statemanagement solution you should use for your application. If
you work in an organization, it will most likely be that the team will
dictate that decision, or maybe they already have a code base that
use Redux or another state manager. Just remember that there are
alternative like MobX²¹, pullstate²² and others. You should at
¹⁴https://reactjs.org ¹⁵https://vuejs.org
¹⁶https://facebook.github.io/flux ¹⁷https://redux.js.org
¹⁸https://vuex.vuejs.org ¹⁹https://vuejs.org/v2/guide/reactivity.html
²⁰https://reactjs.org/docs/hooks-state.html ²¹https://mobx.js.org
²²[https://lostpebble.github.io/pullstate Chapter 6- Introducing State
Management 56 least research and analyze the pros/cons of each
and decide which might best serve your specific needs. In this book,
we’ll start by using a library called Redux Toolkit²³ which makes
working with Redux much simpler. We’ll implement our own peculiar
centralized state manager by leveraging Redux Toolkit that will help
us deliver the goals of this book. For this, we’ll create a set of
interfaces and a structure that will allow use to keep our state
manager organized into modules/domains. Note: Just remember to
be open minded to different ideas, but also challenge them, and
take time to explore your own ideas as well. Different patterns and
code organization strategy can be implemented, and some might be
better or worse than others. This is always the case when writing
code in general, but even more important when writing state
managements patterns. Let’s start by stopping the running
application and installing the required npm packages for Redux
Toolkit which are react-redux and @reduxjs/toolkit: npm install
@reduxjs/toolkit react-redux Nowlet’s proceed creating our store
interfaces and implementations. Store Interfaces Onething I learned
from my past experience using React, Angular, Vue.js, Svelete, and
more, is that there are some advantages adopting a certain flow
that is closer to Flux, but does not have to follow it to the letter. We
definitely won’t need this in every component, as in some cases
using just local state is the right thing to do. But we’ll need it for
global state changes on which many components within the same
app will depend on. One things that often drives the code pattern is
the framework itself. Especially React has a peculiar way as a lot of
plumbing has to happen within the React context itself for React to
be awareof changes. Other frameworks are more flexible in this
(Vue 3 reactive for example) and are less prone to drive your
architectural and patterns decisisions, thus allowing more easily to
decouple your state manager from the actual framework. There are
many libraries out there that have been trying to improve
decoupling React from the state manager. You are again welcome to
research them and explore different ideas etc. In this chapter we’ll
offer a bit of an opinionated structure, but I found that is helps
better understanding how the data and events flow, especially to
beginners. Let’s try to implement a state manager that follow more
or less this pattern: ²³https://redux-toolkit.js.org Chapter 6-
Introducing State Management 57 • wewill invoke an action on our
state manager from a component • the state manager will perform
some tasks within that action • the state manager will commit a
change to our state • the state manager will be organized into
modules (each module will represent a domain/area of the
application. I.e. items, authors, companies, projects, products,
categories, etc) Start creating all the interfaces we need so we can
better understand the abstraction of what we are trying to
accomplish. Items Store Interfaces We will create first the interfaces
for the items store module. Create the directory src/s tore/items.
Inside here, create a directory called models.
ItemsState.interface.ts Here add a file called ItemsState.interface.ts
and paste the following code in it: Chapter 6- Introducing State
Management 58 // file:
src/store/items/models/ItemsState.interface.ts import
{ ItemInterface } from '../../../models/items/Item.interface' /** *
@name ItemsStateInterface * @description Interface represnets our
Items state */ export interface ItemsStateInterface { loading:
boolean items: ItemInterface[] } In the code above we just export
an interface that represents our Items domain state. This will be an
object with a property called items which will contain an array of
objects of type ItemInterface., and one called loading which is a
boolean and will indicate if we are loading data or not (so that we
can eventually display a loading indicator/component in the UI).
Finally, let’s add an index.ts²⁴ to just export our interfaces: // file:
src/store/items/models/index.ts export * from
'./ItemsState.interface' Root Store Interfaces Create now the root
store interfaces. Create the directory src/store/root. Inside here,
create a directory called models. ²⁴index.ts files that just export
code from the same folder are called “Barrels” files Chapter 6-
Introducing State Management 59 RootStore.interface.ts Here add a
file called RootState.interface.ts and paste the following code in
it: // file: src/store/root/models/RootStore.interface.ts import
{ ItemsStoreInterface } from '../../items' // additional domain store
interfaces will be imported here as needed /** * @name
RootStoreInterface * @description Interface represents our root
state manager (store) */ export interface RootStoreInterface
{ itemsStore: ItemsStoreInterface // additional domain store
modules will be added here as needed } Note that this interface will
represent an object that wrap references to each individual domain
module. In this case, we only have one for now called itemsStore.
Here too add a barrel index.ts file to just export the RooStore
interface: Chapter 6- Introducing State Management 60 // file:
src/store/root/models/index.ts export * from './RootStore.interface'
Store Implementation Nowlet’s write the implementations for our
interfaces. Items Store instance Let’s implement the items store
module. Items.slice.ts The term slice is specific to Redux Toolkit.
Ultimately what we care about is that the reducers in here are just
used to perform the final mutations to our state in a synchronous
way. Within the directory, create a file called Items.slice.ts and
paste the following code in it: // src/store/items/Items.slice.ts //
import createSlice and PayloadAction from redux toolkit import
{ createSlice, PayloadAction } from '@reduxjs/toolkit' // import out
items state interface, and the item interface import
{ ItemsStateInterface } from './models' import { ItemInterface }
from '../../models/items/Item.interface' // create an object that
represents our initial items state const initialItemsState:
ItemsStateInterface = { loading: false, items: [] } // create the
itemsStoreSlice with createSlice: export const itemsStoreSlice =
createSlice({ name: 'itemsStoreSlice', initialState: initialItemsState,
reducers: { // reducers are functions that commit final mutations to
the state Chapter 6- Introducing State Management 61 // These will
commit final mutation/changes to the state setLoading: (state,
action: PayloadAction) => { state.loading = action.payload },
setItems: (state, action: PayloadAction) => { // update our state: //
set our items state.items = action.payload || [] // set loading to false
so the loader will be hidden in the UI state.loading = false },
setItemSelected: (state, action: PayloadAction) => { const item =
action.payload const found = state.items.find((o) => o.id ===
item.id) as ItemInterface found.selected = !found.selected } } })
Asyoucanseeinthecodeabove,weuseReduxToolkitcreateSlicetosetupo
urstoremodule by specifying the name, the initialState, and the
reducers. Reducers are simply functions that commit the final
mutations to the state. Reducer is a terminology specific to Redux.
These are usually called in other ways in other state management
frameworks (mutations in Vuex for example). It helps thinking of
reducers as function that commit the final changes to
yourstateinasynchronousway,andtheyareonlyinvokedfromthestoreac
tions(which, on the other hand, are asynchronous). Note that in the
store implementation (Items.store.ts) we’ll extract the slice
“actions” into a lo cal variable
namedmutationstoavoidconfusion(i.e.const mutations =
itemsStoreSlice.actions) Items.store.ts Add another files called
Items.store.ts and paste the following code in it: Chapter 6-
Introducing State Management 62 // src/store/items/Items.store.ts //
import hooks useSelector and useDispatch from react-redux import
{ useSelector } from 'react-redux' import { Dispatch } from 'react' //
import a reference to our RootStateInterface import
{ RootStateInterface } from '../root' // import a reference to our
ItemInterface import { ItemInterface } from
'../../models/items/Item.interface' // import a refence to our
itemsStoreSlice import { itemsStoreSlice } from './Items.slice' /** *
@name useItemsActions * @description * Actions hook that allows
us to invoke the Items store actions from our components */ export
function useItemsActions(commit: Dispatch) { // get a reference to
our slice actions (which are really our mutations/commits) const
mutations = itemsStoreSlice.actions // our items store actions
implementation: const actions = { loadItems: async () => { // set
loading to true commit(mutations.setLoading(true)) // mock some
data const mockItems: ItemInterface[] = [{ id: 1, name: 'Item 1',
selected: false }, { id: 2, name: 'Item 2', selected: false }, { id: 3,
name: 'Item 3', selected: false }] Chapter 6- Introducing State
Management 63 // let's pretend we called some API end-point // and
it takes 1 second to return the data // by using javascript setTimeout
with 1000 for the milliseconds option setTimeout(() => { // commit
our mutation by setting state.items to the data loaded
commit(mutations.setItems(mockItems)) }, 1000) },
toggleItemSelected: async (item: ItemInterface) =>
{ console.log('ItemsStore: action: toggleItemSelected', item)
commit(mutations.setItemSelected(item)) } } // return our store
actions return actions } // hook to allows us to consume read-only
state properties from our components export function
useItemsGetters() { // return our store getters return { loading:
useSelector((s: RootStateInterface) => s.itemsState.loading), items:
useSelector((s: RootStateInterface) => s.itemsState.items) } } /** *
@name ItemsStoreInterface * @description Interface represents our
Items store module */ export interface ItemsStoreInterface
{ actions: ReturnType // use TS type inference getters: ReturnType //
use TS type inference } Weare following a pattern here where we
export two hooks: • useItemsActions (used to initiate a state
change from components or other store modules) • useItemsGetters
(used to retrieve data from the store only from components)
Chapter 6- Introducing State Management 64 This gives us the
power to use actions also from both components and other store
modules. Additionally, use getters only from components. Note that
in the code above, we also export an interface called
ItemsStoreInterface leveraging TypeScript type inference. Here too
add a barrel index.ts file to export the itemsStoreSlice instance and
useItemsStore hook: // file: src/store/items/index.ts export * from
'./Items.slice' export * from './Items.store' Root Store Instance Let’s
now implement our root store instance. Root.store.ts Inside the
directory src/store/root add a file called Root.store.ts and paste the
following code in it: // file: src/store/root/Root.store.ts // import
configureStore from redux toolkit import { configureStore } from
'@reduxjs/toolkit' import { useDispatch } from 'react-redux' //
import our root store interface import { RootStoreInterface } from
'./models' // import our modules slices and actions/getters import
{ itemsStoreSlice, useItemsActions, useItemsGetters } from
'../items/' // configure root redux store for the whole app. // this will
be consumed by App.tsx export const rootStore =
configureStore({ reducer: { // add reducers here itemsState:
itemsStoreSlice.reducer // keep adding more domain-specific
reducers here as needed } Chapter 6- Introducing State
Management 65 }) // Infer the `RootStateInterface` type from the
store itself (rootStore.getState) // thus avoiding to explicitely
having to create an additional interface for the export type
RootStateInterface = ReturnType // hook that returns our root store
instance and will allow us to consume our app st\ ore from our
components export function useAppStore(): RootStoreInterface { //
note: we are callin dispatch "commit" here, as it make more sense
to call it th\ is way // feel free to just call it dispatch if you prefer
const commit = useDispatch() return { itemsStore: { actions:
useItemsActions(commit), getters: useItemsGetters() }, // additional
domain store modules will be added here as needed } } // infer the
type of the entire app state type IAppState = ReturnType /** *
@name getAppState * @description * Returnss a snapshot of the
current app state (non-reactive) * This will be used mainly across
store modules (i.e. items/etc) * In components we'll usually use
getters, not this. * @returns */ export function getAppState():
IAppState { const appState = rootStore.getState() return
{ ...appState } } In the code above, notice how we ultimately export
a hook called useAppStore. This will return our root store that
conains all the domain-specific stores (itemsStore etc). Here we use
Chapter 6- Introducing State Management 66 the interface
RootStoreInterface which was created earlier through TypeScript
inference. Additionally, we also export function called getAppState
that returns a read-only non reactive snapshot of the current state.
This allows us to read the state from other store modules. We
should not use this in components but only from other store
modules. Components will most of the time use only getters. Add a
barrel index.ts file to export our root store hooks and
getAppState: // file: src/store/root/index.ts export * from
'./Root.store' Up one directory, finally add one last barrel index.ts
file at src/store/index.ts to export only the root store: // file:
src/store/index.ts export * from './root' Let’s now go back to our
components and start consuming our state. App.tsx First we have to
modify our App.tsx code so we can “provide” the Redux store to our
React app. Add the following two imports to get a reference to the
Redux Provider, and a reference to out rootStore instance: // file:
App.tsx // import a reference to Redux Provider and our rootStore
import { Provider } from 'react-redux' import { rootStore } from
'./store' ... Then, in the render function we need to wrap the existing
root
with our Redux Provider: Chapter 6- Introducing State Management
67 // file: App.tsx ... return ( {/* wrap the root App element with
Redux store provi\ der */}

...

) ... For now just save. We are going to add another component
called Items.view.tsx and then come back to our App.tsx for more
changes. Items.view.tsx First we are going to add a new directory
called views under src. Here we add a new higher-level component
called Items.view.tsx. Your directory structure will be like this:
NotethatinReactanythingisacomponentandwecouldhavejustcalledthi
sItems.component.tsx
andputitundercomponent/items.Thisisonlyfororganizational
purposes. We arereally free to organize the code as we see fit. In
this case I also wanted to better separate what the lower
components are doing and accessing the global state only in the
higher-level component. Chapter 6- Introducing State Management
68 Paste the following code within the file Items.view.tsx: // file:
src/views/Items.view.tsx // import hook useEffect from react import
{ useEffect } from 'react' // import a reference to our ItemInterface
import { ItemInterface } from '../models/items/Item.interface' //
import a reference to your ItemsList component: import
{ ItemsListComponent } from
'../components/items/ItemsList.component' // import our
useAppStore hook from our store import { useAppStore } from
'../store' // ItemsView component: function ItemsView() { // get a
reference to our itemsStore instanceusing our useAppStore() hook:
const { itemsStore } = useAppStore() // get a reference to the items
state data through our itemsStore getters: const { loading, items }
= itemsStore.getters // item select event handler const onItemSelect
= (item: ItemInterface) =>
{ itemsStore.actions.toggleItemSelected(item) } // use React
useEffect to invoke our itemsStore loadItems action only once after
t\ his component is rendered: useEffect(() =>
{ itemsStore.actions.loadItems() }, []); // <-- empty array means
'run once' // return our render function containing our
ItemslistComponent as we did earlier \ in the App.tsx file return (

Chapter 6- Introducing State Management 69 ) } export default


ItemsView In the code above we are basically rendering the same
itemsListComponent as we did earlier in the App.tsx file. However,
here we are consuming the data from our items store and invoking
our store actions that will mutate our data. Back to the App.tsx file
Let’s finally modify the App.tsx so we can consume our ItemsView
component in it. Replace the entire content of the file with this: //
file: App.tsx // import our app.css import './App.css' // import a
reference to Redux Proivder and our rootStore import { Provider }
from 'react-redux' import { rootStore } from './store' // import a
reference to our ItemsView component import ItemsView from
'./views/Items.view' // App component: function App() { return ( {/*
wrap the root App element with Redux store provi\ der */}

) } export default App Save the file. Chapter 6- Introducing State


Management 70 WebBrowser The webbrowser should refresh and
display the content similar to before. Notice that now it will take
about 1 second before the items will be rendered. This is because in
our loadItems action implementation we used a setTimeout with a 1
second delay to simulate a possible call to an API for example.
ItemsList.component.tsx Let’s add a property to our
ItemsList.component.tsx called loading of type boolean: // file:
ItemsList.component.tsx ... export class ItemsListComponent
extends React.Component void }> { constructor(props: { loading:
boolean, // add this items: ItemInterface[], onItemSelect: (item:
ItemInterface) => void }) { super(props) } ... Nowwithin the

element, add a one-way binding using the single curly braces to


print out the value of the loading property: Chapter 6- Introducing
State Management 71 // file: ItemsList.component.tsx ... render():
React.ReactNode { const { loading, items } = this.props // include
loading here return

Items- loading: { String(loading) }:

... Back to the WebBrowser Now, when werefresh the browser, we’ll
first see a blank list, but in the header we’ll see the text My items-
loading: true: After 1 second the items will render and the h3
element will display the text My items loading: false: Loader
Component Let’s create a quick-and-dirty loader component that we
can show to indicate a loading operation. Create the directory
src/components/shared. Within this directory create a file called
Loader.component.tsx. Within the file, paste the following code:
Chapter 6- Introducing State Management 72 // file:
Loader.component.tsx import React from 'react' // Loader
component export class Loader extends React.Component
{ render(): React.ReactNode { return

} } Save the file. Now open the App.css file and append the
following css to the existing code: /* begin: loader component
*/ .loader { display: inline-block; } .loader .bounceball { position:
relative; width: 30px; } .loader .bounceball:before { position:
absolute; content: ''; top: 0; width: 30px; height: 30px; border-
radius: 50%; background-color: #61dafa; transform-origin: 50%;
animation: bounce 500ms alternate infinite ease; } @keyframes
bounce { 0% { top: 60px; height: 10px; border-radius: 60px 60px
20px 20px; transform: scaleX(2); } 25% { height: 60px; Chapter 6-
Introducing State Management 73 border-radius: 50%; transform:
scaleX(1); } 100% { top: 0; } } /* end: loader component */ This
provides a basic loader that uses pure CSS for the animation. You
are free to use an animated gif, or svg image, or font-icon etc. In
later chapter we might modify this to implement a versin that uses
TailwindCSS. Now,lets go back into our ItemsList.component.tsx
code and import a reference to our new Loader component, and
update our render() function as follow (complete code): // file:
ItemsList.component.tsx import React from 'react' // import
reference to our interface import { ItemInterface } from
'../../models/items/Item.interface' // import reference to your Item
component: import { ItemComponent } from
'./children/Item.component' // import a reference to our Loader
component: import { Loader } from '../shared/Loader.component' //
ItemsList component export class ItemsListComponent extends
React.Component void }> { constructor(props: { loading: boolean,
items: ItemInterface[], onItemSelect: (item: ItemInterface) =>
void }) { super(props) } handleItemClick (item: ItemInterface)
{ this.props.onItemSelect(item) } Chapter 6- Introducing State
Management 74 render(): React.ReactNode { const { loading,
items } = this.props let element if (loading) { // render Loader
element = } else { // render

element =

{ items.map((item, index) => { return this.\


handleItemClick(item)}> }) }

} return
Items- loading: { String(loading) }:

{element}

} } Savethefile andtherefreshed
thewebpagewillshowtheloaderbouncingforabout1second before it
renders the items: Then the loader will hide and the items list is
rendered: Chapter 6- Introducing State Management 75
Congratulations on completing this chapter and learning how to
build a state manager organized into domains to easily manage the
application state in a consistent and predictable way. It’s a long
chapter, the concepts outlined here require a lot of code to
implement, and not everyone gets through it in a straightforward
fashion the first time around. In the next chapters we will try to
improve this code even more so arm yourself with a lot of patience!
Chapter 6- Introducing State Management 76 Chapter 6 Recap
WhatWeLearned • How to create a centralized state manager
organized into modules, leveraging Redux Toolkit • Howto use our
state manager to update our Items state • Howtocreateactions and
reducers (reducers are just final mutations/commits of state
changes) • Howto invoke state actions from our components •
Howtouse aloading property on our state to provide feedback to the
user about long running processes through a loader (animation) •
Howto create a simple and reusable Loader component
Observations • We are still using hard-coded data (mockItems
within the actions in the store/item s/Items.store.ts file), instead of
loading the data through an API client
Basedontheseobservations,thereareafewimprovementswewillmakei
nthenextchapters: Improvements • Create an API client that can
serve mocked data for quick front-end development and
prototyping, and an API client that can communicate with real API
end-points Chapter 7- Api Client So far
wehaveworkedbymanipulatingtheappstate/data through our state
manager (store). However, we are still ”pretending” to load data by
using a mockItems variable with hard coded mock data within our
loadItems action, and using the setTimeout trick to add a bit of
delay before returning the data (so we have at least 1 second to
show our Loader to the user). In the real world, we’ll be most likely
writing a component that has to load the data from a server-side
API end-point. At the same time, we do not want to lose our ability
to do quick prototyping and development of our front-end, even if
the server-side API has not been developed yet. Now there are
different ways of accomplishing this. Some people like to use mock
data returned by a real API (there are packages and services out
there that do just this²⁵). Others prefer to have 2 different
implementations for each API client, one that returns the mocked
data (either by loading from disk or invoking a mocked API service),
and one that returns the live data from the real server API. We’ll be
implementing the latter pattern in this chapter so we have better
control on our data and also have better control on different
scenarios. Another pattern is to create a separate API client for
each area of our application. This will enable for better separation
of concerns, avoid code cluttering, more easily write unit tests
against each client. This is the pattern we’ll be following in this
book, but remember this is not the only way to accomplish this. You
should always evaluate what is the best solution for your specific
requirements and evaluate that it fits your needs. You should also
read about Domain Driver Design, even though this book is not
strictly following DDD principles, still the overall idea here is to try
to keep code organized by application domain. API Client Overview
Here is an overview of our API client architecture: ²⁵JsonPlaceHolder
or miragejs for example Chapter 7- Api Client 78 API Client module
will read the custom environment variable called VITE_API_CLIENT
and there are two possible outcomes: • when VITE_API_CLIENT is
Mock: it will return the Mock API Client • when VITE_API_CLIENT is
Live: it will return the Live API Client Chapter 7- Api Client 79
Domains We’ll create a global ApiClient that wraps additional clients
organized by application domain. Our ApiClient will have for
example a property called items which is the actual API client for
the Items domain. As our application grows, we’ll be adding more
domains specific API clients. Our goal is to eventually consume our
API client code from our store in this way:
apiClient .items .fetchItems()
HerewehaveaninstanceofourmainApiClientInterface.Wethenaccessit
sitemsproperty which is the domain-specific API client (of type
ItemsApiClientInterface) and call its methods or access its
properties. Later, if for example need to add a new people domain,
we will add a people property to our main ApiClientInterface that
points to an instance of PeopleApiClientInterface. Then we will be
able to call its methods like this: apiClient .people .fetchPeople() As
you can see, this makes the code much more concise and readable.
NOTE: This might seem to complicate things at first. However,
remember that the scope of this book is to build a foundation for
large-scale applications. Our primary goal is a solid code
organization and structuring to avoid cluttering as the code might
grow very large with many files. The MainApiClient
Createthedirectorysrc/api-client/models.Insidethisdirectory,createth
efileApiClient.interface.ts with the following code: Chapter 7- Api
Client 80 // file: src/api-client/models/ApiClient.interface.ts import
{ ItemsApiClientInterface } from './items' /** * @Name
ApiClientInterface * @description * Interface wraps all api client
modules into one places for keeping code organized. */ export
interface ApiClientInterface { items: ItemsApiClientInterface } As
you can see in the code above, our ApiClient will have a property
called items of type ItemsApiClientInterface, which will be the API
client specific to the Items domain. Nowlet’s create the the Items
API client. Items domain Api Client Nowwecreate the interfaces and
model that defines a domain-specific API client. Create the directory
src/api-client/models/items. Inside thisd directory, create the follow
ing files: • index.ts • ItemsApiClient.interface.ts •
ItemsApiClient.model.ts • ItemsApiClientOptions.interface.ts Your
directory structure will look like this: Following is the the
description and code for each of the files.
ItemsApiClientOptions.interface.ts In order to avoid using hard-
coded strings, and to enforce type-checking at development time,
we’ll be using interface ItemsApiClientOptionsInterface for the
values that indicates the API end-points consumed by the
ItemsApiClient. Also, we’ll have a mockDelay parameter that we can
use to simulate the delay when loading data from static json files.
Here is the code: Chapter 7- Api Client 81 // file:
src/api-client/models/items/ItemsApiClientOptions.interface.ts /** *
@Name ItemsApiClientEndpoints * @description * Interface for the
Items urls used to avoid hard-coded strings */ export interface
ItemsApiClientEndpoints { fetchItems: string } /** * @Name
ItemsApiClientOptions * @description * Interface for the Items api
client options (includes endpoints used to avoid hard\-coded
strings) */ export interface ItemsApiClientOptions { mockDelay?:
number endpoints: ItemsApiClientEndpoints }
ItemsApiClient.interface.ts This is the interface for our
ItemsApiClient. Our interface requires implementing a method
called fetchItems the will return a list of items. Here is the code to
paste into ItemsApi Client.interface.ts: // file:
src/api-client/models/items/ItemsApiClient.interface.ts import
{ ItemInterface } from '../../../models/items/Item.interface' /** *
@Name ItemsApiClientInterface * @description * Interface for the
Items api client module */ export interface ItemsApiClientInterface {
fetchItems: () => Promise } Chapter 7- Api Client 82
ItemsApiClient.model.ts This is the model (class) for our
ItemsApiClient which implements our Items API client interface. For
the initial version of this, we will be using a third-part open-source
NPM package called axios. This is just a library that allows to make
Ajax call in a much easier way. Let’s go back to the terminal, from
within my-react-project directory, and install axios with the
command: npm install axios--save NOTE: we will improve this even
more later to avoid having references to a third-party NPM package
spread throughout the code. Also note, we are showing here to use
a 3rd party package like axios on purpose, instead of the browser
built-in fetch api, to show in later chapters how we should always
try to abstract and encapsulate dependencies to avoid polluting our
code. Backtotheeditor, open ItemsApiClient.model.ts and start
importing all the things we need: // file:
src/api-client/models/items/ItemsApiClient.model.ts import axios,
{ AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
import { ItemsApiClientOptions, ItemsApiClientEndpoints } from
'./ItemsApiClientOpti\ ons.interface' import
{ ItemsApiClientInterface } from './ItemsApiClient.interface' import
{ ItemInterface } from '../../../models/items/Item.interface' … And
here is the class that implement our ItemsApiClientInterface:
Chapter 7- Api Client 83 // file:
src/api-client/models/items/ItemsApiClient.model.ts ... /** * @Name
ItemsApiClientModel * @description * Implements the
ItemsApiClientInterface interface */ export class
ItemsApiClientModel implements ItemsApiClientInterface { private
readonly endpoints!: ItemsApiClientOptions private readonly
mockDelay: number = 0 constructor(options: ItemsApiClientOptions)
{ this.endpoints = options.endpoints if (options.mockDelay)
{ this.mockDelay = options.mockDelay } } fetchItems(): Promise
{ return new Promise((resolve) => { const endpoint =
this.endpoints.fetchItems // axios options const options:
AxiosRequestConfig = { headers: { } } axios .get(endpoint, options)
.then((response: AxiosResponse) => { if (!this.mockDelay)
{ resolve(response.data as ItemInterface[]) } else { setTimeout(()
=> { resolve(response.data as ItemInterface[]) }, this.mockDelay) }
}) .catch((error: any) => { console.error('ItemsApiClient: HttpClient:
Get: error', error) }) Chapter 7- Api Client 84 }) } } index.ts (barrel
file) This just exports all our interfaces and
modelsunderitems/sothatwecanmoreeasilyimport them later in
other parts of the code: // file: src/api-client/models/items/index.ts
export * from './ItemsApiClientOptions.interface' export * from
'./ItemsApiClient.interface' export * from './ItemsApiClient.model'
MockandLive Api Clients Now that we have defined our models for
ApiClientInterface and ItemsApiClientInter face, let’s implement a
mechanism that will allow us to either use a mock api-client that
returns static json data, or a live api-client that returns data from as
real API. Under the src/api-client directory, create two new sub-
directories called: • mock (this will contain our mock
implementations to return static json data) • live (this will contain
the implementation that call the real API end-points) We’ll be
writing a mock implementation of our ApiClientInterface and its
child Item sApiClientInterface. We’ll be also instantiating either the
mock or live api-client based on config.. MockApiClient Items domain
mockAPIinstance Within the mock directory, add a child directory
called items, and within that one create a new file named index.ts.
Your directory structure should look like this: Chapter 7- Api Client
85 Inside the src/api-client/mock/items/index.ts file, paste the
following code: // file: src/api-client/mock/items/index.ts import
{ ItemsApiClientOptions, ItemsApiClientInterface,
ItemsApiClientModel } from '../../models/items' const options:
ItemsApiClientOptions = { endpoints: { fetchItems: '/static/mock-
data/items/items.json' }, mockDelay: 1000 } // instantiate the
ItemsApiClient pointing at the url that returns static json mock \
data const itemsApiClient: ItemsApiClientInterface = new
ItemsApiClientModel(options) // export our instance export
{ itemsApiClient } Here we import all our interfaces and models,
then we instantiate a variable called options Chapter 7- Api Client
86 of type ItemsApiClientOptions that holds the API end-points
values and the mockDelay option. In this case, since this is the mock
implementation, for fetchItems we will point to some static json file
with the mock data. Note that we have only fetchItems, but we
could have multiple end-points. For now we’ll focus only on
returning data. Later, in more advanced chapter I’ll show you how to
do something similar for CRUD operations. Wethencreate aninstance
of our ItemsApiClient class by passing our options instance into the
constructor (as you can see, later in our live implementation we’ll
pass an instance of ItemsApiClientOptions that contains the
paths/urls to the real end-points) Finally, we just export our
instance called itemsApiClient. MockAPIinstance Now let’s move one
directory up, under src/api-client/mock and create another index.ts
file here. Your directory structure should look like this: Inside the
src/api-client/mock/index.ts file, paste the following code: // file:
src/api-client/mock/index.ts import { ApiClientInterface } from
'../models/ApiClient.interface' import { itemsApiClient } from
'./items' // create an instance of our main ApiClient that wraps the
mock child clients const apiMockClient: ApiClientInterface = { items:
itemsApiClient } // export our instance Chapter 7- Api Client 87
export { apiMockClient } This is the mock implementation of our
main ApiClient that wraps that items client.
HereweimportourApiClientInterfaceinterface,andourmockinstanceof
ItemsApiClient. Wethencreate an instance of our ApiClientInterface
that is called apiMockClient because it will use the mock
implementation of the ItemsApiClient. Live Api Client Items domain
live API instance Similar to what we did with our mock api client,
we’ll be implementing the live api client now. Note that the live
directory structure will be the same as the mock directory structure.
Create directory src/api-client/live/items and here add a new file
named index.ts. Your directory structure should look like this: Inside
the src/api-client/live/items/index.ts file, paste the following code:
Chapter 7- Api Client 88 // file: src/api-client/live/items/index.ts
import { ItemsApiClientOptions, ItemsApiClientInterface,
ItemsApiClientModel } from '../../models/items' const options:
ItemsApiClientOptions = { endpoints: { // this should be pointing to
the live API end-point fetchItems: '/path/to/your/real/api/end-point' }
} // instantiate the ItemsApiClient pointing at the url that returns
live data const itemsApiClient: ItemsApiClientInterface = new
ItemsApiClientModel(options) // export our instance export
{ itemsApiClient } NOTE: this code is almost exactly the same as the
mock client. The only difference is the fetchItems property that here
says for now “/path/to/your/real/api/end-point”. You’ll replace this
with the actual value of your real server API end-point url/path. If
you do not have one yet, leave the current value as a place holder
and updated once in the future you’ll have your server API ready.
Live API instance Nowlet’s move one directory up, under src/api-
client/live and create another index.ts file here. Your directory
structure should look like this: Chapter 7- Api Client 89 Inside the
src/api-client/live/index.ts file, paste the following code: // file:
src/api-client/live/index.ts import { ApiClientInterface } from
'../models' // import module instances import { itemsApiClient }
from './items' // create an instance of our main ApiClient that wraps
the live child clients const apiLiveClient: ApiClientInterface =
{ items: itemsApiClient } // export our instance export
{ apiLiveClient } This code is also almost identical to the related
mock index.ts file. The only exceptions are: 1. We use the live
ItemsApiClient from api-client/live-items 2. We name the instance
apiLiveClient for more clarity Wethen just export our apiLiveClient
instance. In a bit we’ll be adding one final index.ts at the root of
src/api-client that will act as our API client “provider”. This will
return either the mock or the live instance based on an environemnt
variable. So let’s first setup some things to work with environment
variables. Chapter 7- Api Client 90 Environment Variables Since Vite
uses dotenv²⁶ to load environment variables, we’ll have to create
two .env files²⁷ at root of your src
directory: .env.dev .env.production # loaded when mode is dev for
local development # loaded when mode is production Inside
the .env.mock put the following: # file src/.env.dev
VITE_API_CLIENT=mock Inside the .env.production put the following:
# file src/.env.production VITE_API_CLIENT=live You might have to
add declarations for the import.meta.env types within the src/vite
env.d.ts file²⁸: // file: src/vite-env.d.ts /// /// // types for Vite env
variables: // (reference: https://vitejs.dev/guide/env-and-
mode.html#intellisense-for-typescrip\ t) interface ImportMetaEnv
{ readonly VITE_API_CLIENT: string // more env variables... }
interface ImportMeta { readonly env: ImportMetaEnv }
²⁶https://github.com/motdotla/dotenv ²⁷https://vitejs.dev/guide/env-
and-mode.html#production-replacement
²⁸https://vitejs.dev/guide/env-and-mode.html#intellisense-for-
typescript Chapter 7- Api Client 91 NOTE: Only variables prefixed
with VITE_ are exposed to the Vite-processed code.²⁹ We’ll be now
able to access the value of our environment variables in TypeScript
with import.meta.env (i.e. import.meta.dev.VITE_API_CLIENT).
Before we can do this, we need to do one final change in our
package.json scripts configurationso it will correctly set the
expected environment variables when running locally for
development with npm start, or
whenbuildingforproductionwithnpmrunbuild.Thecurrentcontentofyo
urscriptsection should be like this: file: package.json ... "scripts":
{ "start": "npm run dev", "dev": "vite--mode mock", // here add--
mode mock "build": "tsc && vite build--mode production", // here
add--mode production ... }, ... Change the dev command to: "dev":
"vite--mode mock", Change the build command to: "build": "tsc &&
vite build--mode production" Optional: You could also addabuild-
mockcommandthatusesthemockapiclient,ifyouare do not plan to
have a real API in your project, or maybe to test new front-end
functionality in production when the server API is not yet ready:
"build-mock": "tsc && vite build--mode mock" Note: when running
the app, if you make a change to the–mode value in the
package.json, or the values within the .env files, you’ll have to stop
it with CTRL+C and restart with npm start for changes to take into
effect. One last thing: we put our .env files within the src/ directory
for now. To make sure Vite is aware of where they are, open the
vite.config.ts file and make sure the envDir option is configured with
the following value (we added this at the end of Chapter 5, but is a
good idea to verify that is there): ²⁹import.meta.env Chapter 7- Api
Client 92 // file: vite.config.js /// import { defineConfig } from "vite"
import reactRefresh from "@vitejs/plugin-react-refresh" //
https://vitejs.dev/config/ export default defineConfig({ plugins:
[reactRefresh()], envDir: './src/' // <-- make sure this is there }) To
test that the configuration is working, temporarily modify the
App.tsx code to ourput all the content of the import.meta.env like
this: // file: src/App.tsx ...

[{JSON.stringify(import.meta.env)}] ... Stop the app with CTRL+C


and run it again with npm start. Verify that in the browser our
App.tsx renders something like this at the top:
[{"VITE_API_CLIENT":"mock","BASE_URL":"/","MODE":"mock","DEV":t
rue,"PROD":false, ... As you can see, our VITE_API_CLIENT
environment variable contains the correct value “mock” and we are
able to read this in our views or other client-side code. Now remove
the code we just added to App.tsx and let’s proceed creating our Api
Client Provider. Api Client Provider Now we need one final index.ts
that will server our main API client factory and return either the
mock or the live API client based on an environment variable (later
you might Chapter 7- Api Client 93 find easier to drive this with
different configuration files). Create an the file at the root of src/api-
client: Inside the src/api-client/index.ts file, import a reference to
our ApiClientInterface inter face, and both the instances for the
mock and the live clients: // file: src/api-client/index.ts import
{ ApiClientInterface } from './models' import { apiMockClient } from
'./mock' import { apiLiveClient } from './live' ... Now we will add
some code that will export either the mock or live clients based on
the VITE_API_CLIENT environment variable: // file:
src/api-client/index.ts ... let env: string = 'mock' // Note: Vite uses
import.meta.env (reference: https://vitejs.dev/guide/env-and-
mode\ .html) // optional: you can console.log the content of
import.meta.env to inspect its value\ s like this:
console.log('import.meta.env', JSON.stringify(import.meta.env)) if
(import.meta.env && import.meta.env.VITE_API_CLIENT) { env =
import.meta.env.VITE_API_CLIENT.trim() } // return either the live or
the mock client let apiClient: ApiClientInterface if (env === 'live')
{ Chapter 7- Api Client 94 apiClient = apiLiveClient } else { //
default is always apiMockClient apiClient = apiMockClient } export {
apiClient } Now let’s proceed to update our store to consume the
data from our newly created Api Client. Store Instance updates Back
into our src/store/items/Items.store.ts code, we can now finally
remove the reference to the hard-coded data and use our new API
client to retrieve these data. Start by adding an import for our
apiClient (note how we no longer have to worry about using the
mock or the live one, the system we’ll handle that automatically
based on the VITE_API_CLIENT environment variable we created
earlier): // src/store/items/Items.store.ts ... // import a reference to
our apiClient instance import { apiClient } from '../../api-client' ...
Then, within the loadItems action, remove the hard-coded
mockItems variable and its data. Then remove the setTimeout lines
with the call to commit(setItems(mockItems)). Replace
theloadItemscodewithacalltoapiClient.items.fetchItemsandthistimed
ispatch/ commit setItems passing it the data returned by our
fetchItems: Chapter7-ApiClient 95
//src/store/items/Items.store.ts ...
//ouritemsstoreactionsimplementation:
constactions:ItemsStoreActionsInterface={
//actionthatweinvoketoloadtheitems fromanapi:
loadItems:async()=>{ //setloadingtotrue
commit(mutations.setLoading(true)) //begin:removecode
//mocksomedata constmockItems: ItemInterface[]=[{ id:1,
name:'Item1', selected:false },{ id:2, name:'Item2', selected:false },
{ id:3, name:'Item3', selected:false }] //let'spretend wecalled
someAPIend-point //andittakes1secondtoreturnthedata
//byusingjavascriptsetTimeoutwith1000forthemillisecondsoption
setTimeout(()=>{
//commitourmutationsbysettingstate.itemstothedataloaded
commit(mutations.setItems(mockItems)) },1000)
//end:removecode //begin:addcode
//invokeourAPIcientfetchItemstoloadthedatafromanAPIend-point
constdata=await apiClient.items.fetchItems()
//commitourmutationsby settingstate.itemstothedataloaded
commit(mutations.setItems(data)) //end:addcode }, Chapter 7- Api
Client 96 ... Wealso need to create a data folder from where our
mock api-client will load the static json files. If you remember,
earlier during our Mock Api Client implementation we set the urls
fetchItems end-point path to be /static/mock-data/items/items.json.
Weneed to create a directory called static under our public folder,
because that is what app considers our root directory to be when
running the application. Within the static directory create a sub-
directory called mock-data, within mock-data add one more sub-
directory called items. Here create a file called items.json. Within
the items.json files and paste in the following data: # File:
public/static/mock-data/items/items.json: [{ "id": 1, "name": "Item
1", "selected": false }, { "id": 2, "name": "Item 2", "selected":
false }, { "id": 3, "name": "Item 3", "selected": false }, { "id": 4,
"name": "Item 4", "selected": false }, { "id": 5, "name": "Item 5",
"selected": false }] Make sure there are no errors in the terminal. If
needed stop it with CTRL-C and run again with npm start. The
browser should display a loader, then render our items list as
before, but this time should display 5 items (because the data now
is loaded through our Api client from the file public/static/mock-
data/items/items.json): Chapter 7- Api Client 97 Notice how powerful
is this pattern we just implemented as it allows us to easily build
our front-end components in isolation without a real API, and later
everything we’ll just work with a live API client that returns the
same data structure as our static json data. Alternatives There are
other ways in which you could use a mocked API. There are services
or libraries out there that can help you build a mocked API like
Miragejs or JSONPlaceHolder³⁰, and you could simplify the code here
by having only one apiClient that uses either mock or live API end-
points based on environment variables only etc. Some of these
alternatives require running an additional server app that will serve
your mocked API. I opted to show you how you can do this using
static .json files that are located in the same project under
public/static/mock-data as this gives you a lot of flexibility to play
around with different things when you are starting out. The other
thing is that by having a specific implementation of the mock
apiClient you do not have to necessarily return the .json files, but
you could simulate fake responses or pretend to have saved or
deleted an item without actually modifying any static data (so it will
be just in memory, and when you refresh the web browser the data
would be reloaded as in its original state). Additionally, this gives
you the flexibility to use either: static JSON files, or maybe for the
url end points use something like Miragejs etc for some of the API
clients. You can research alternatives as you see fit and make the
decision you feel works better for you, but remember you are not
confined to one way or another if you keep following the patterns I
am showing you in this book. Indeed, let me finish by adding a few
more instructions on how to use for example an NPM package called
json-server. ³⁰JsonPlaceHolder or miragejs for example Chapter 7-
Api Client 98 Alternative: using json-server Let’s start by install
json-server: npm install-D json-server Now let’s rename the
vite.config.ts file to vite.config.jsonserver.ts. Make 2 more copies of
this file and name one vite.config.mock.ts and the other
vite.config.production.ts: The content for vite.config.mock.ts will
be: /// /// import { defineConfig } from 'vite' import react from
'@vitejs/plugin-react' import { fileURLToPath, URL } from 'url' //
https://vitejs.dev/config/ export default defineConfig({ plugins:
[react()], envDir: './src/', resolve: { alias: { // @ts-ignore '@':
fileURLToPath(new URL('./src', import.meta.url)), }, }, server: { port:
3000, origin: 'http://localhost:3000/', open: 'http://localhost:3000/' },
test: { Chapter 7- Api Client 99 globals: true, environment: 'jsdom',
exclude: [ 'node_modules' ] } }) The content for
vite.config.production.ts will be: /// /// import { defineConfig } from
'vite' import react from '@vitejs/plugin-react' import { fileURLToPath,
URL } from 'url' // https://vitejs.dev/config/ export default
defineConfig({ plugins: [react()], envDir: './src/', resolve: { alias: { //
@ts-ignore '@': fileURLToPath(new URL('./src',
import.meta.url)), }, }, test: { globals: true, environment: 'jsdom',
exclude: [ 'node_modules' ] } }) The content for
vite.config.jsonserver.ts will be: Chapter 7- Api Client 100 /// ///
import { defineConfig } from 'vite' import react from '@vitejs/plugin-
react' import { fileURLToPath, URL } from 'url' //
https://vitejs.dev/config/ export default defineConfig({ plugins:
[react()], envDir: './src/', resolve: { alias: { // @ts-ignore '@':
fileURLToPath(new URL('./src', import.meta.url)), }, }, server: { port:
3000, origin: 'http://localhost:3000/', open: 'http://localhost:3000/',
proxy: { '/jsonserver': { target: 'http://localhost:3111',
changeOrigin: true, secure: false, ws: false, rewrite: (path) =>
path.replace(/^\/jsonserver/, '') } } }, test: { globals: true,
environment: 'jsdom', exclude: [ 'node_modules' ] } }) Note how the
main difference in the vite.config.jsonserver.ts is the addition of the
proxy section: Chapter 7- Api Client 101 ... ... proxy: { '/jsonserver':
{ target: 'http://localhost:3111', changeOrigin: true, secure: false,
ws: false, rewrite: (path) => path.replace(/^\/jsonserver/, '') } } This
is telling Vite to proxy all the requests for endpoints that start
with /jsonserver to the url http://localhost:3111 (this is where the
json-server API will run from) Modify tsconfig.node.json include
section like this (if you don’t have this file, please create it): { }
"compilerOptions": { "composite": true, "module": "esnext",
"moduleResolution": "node" }, "include":
[ "vite.config.jsonserver.ts", "vite.config.mock.ts",
"vite.config.production.ts" ] Modify tsconfig.json to reference also
tsconfig.node.json: { } ... "references": [{ "path":
"./tsconfig.node.json" }] Modify the script section of the
package.json file to have two additional commands: Chapter 7- Api
Client 102 • with-jsonserver (we’ll use this to run the app using the
vite.config.jsonserver.ts) • json-server-api (with this we’ll start json-
server on port 3111) Also
updatethecurrentcommandtoexplicitelysetwhichViteconfigfile to use
with–config: ... "scripts": { "dev": "vite--config vite.config.mock.ts--
mode mock", "build": "tsc && vite build--config
vite.config.production.ts--mode productio\ n", ", "build-beta": "tsc
&& vite build--config vite.config.production.ts--mode beta\ "build-
local": "tsc && vite build--config vite.config.production.ts--mode loc\
alapis", "build-mock": "tsc && vite build--config vite.config.mock.ts--
mode mock", "preview": "vite preview--config vite.config.mock.ts--
mode mock", "start": "npm run dev", "start-local": "vite--config
vite.config.production.ts--mode localapis", "with-jsonserver": "vite--
config vite.config.jsonserver.ts--mode jsonserver", "json-server-api":
"json-server--port 3111--watch json-server/db.json", ... Create json-
server data under src/json-server/db.json with this: // file: src/json-
server/db.json { "items": [ { "id": 1, "name": "Item 1 from json-
server", "selected": false }, { "id": 2, "name": "Item 2 from json-
server", "selected": false }, { "id": 3, Chapter 7- Api Client 103
"name": "Item 3 from json-server", "selected": false }, { "id": 4,
"name": "Item 4 from json-server", "selected": false }, { "id": 5,
"name": "Item 5 from json-server", "selected": false } ] }
Nowtofinally test it, temporarily modify the file
src/api-client/mock/items/index.ts to use /jsonserver/items for the
fetchItems url: // file: src/api-client/mock/items/index.ts ... const
options: ItemsApiClientOptions = { endpoints: { //fetchItems:
'/static/mock-data/items/items.json' // <-- comment this line out
fetchItems: '/jsonserver/items' // <-- add this line }, mockDelay:
1000 } ... Note: we’ll drive the API urls end-points through a much
better configuration strategy in the next chapters. Nowstop the app,
and this time open 2 terminal windows: • in terminal one, execute
npm run json-server-api (this will run json-server API on port 3111)
• in therminal two, execute npm run with-jsonserver (this will start
our app but tell Vite to use the vite.config.jsonserver.ts which
contains our proxy configuration) Chapter 7- Api Client 104 The
browser should now display:
NOTE:DonotforgettorevertyourchangefortheURLend-
pointwithinthefilesrc/api-client/mock/items/index.ts Later, when we
introduce the apllication configuration in the next chapters, we’ll
drive the end-points from configuration and will not have to modify
eny code to test different environments. Chapter 7- Api Client 105
Chapter 7 Recap WhatWeLearned • Howto implement an apiClient
that automatically can serve either mock or real data depending on
environment variables configuration • How to continue enforcing
type checking at development time with TypeScript inter faces and
models • Howto structure directories and files in an organized way •
Howto invoke our api client from the store Observations •
WehaveareferencetoathirdNPMpackage(axios)inourItemsApiClientm
odeandif we keep following this pattern we’ll keep polluting new api
client implementations for different areas with references to this
NPM package in several parts of our code. This will cause a build up
in technical debt that will make it harder to later replace axios with
something else one day we’ll have to. This might happen either
because axios will no longer be supported, or maybe better NPM
packages will be available that we want o use in its place. Either
way, we should structure our code in a way so that we can more
easily replace axios with something else without having to change a
lot of code in too many places. Based on these observations, there
are a few improvements that will be making into the next two
chapters: Improvements • Create an HttpClient model that
implements an HttpClientInterface where we can encapsulate the
reference to axios all in one place and make it easier to change later
if we find the need to use a different NPM package. Chapter 8-
Enhance the Api Client From the previous chapter recap, we
observed that the ItemsApiClient contains hard-coded references to
the axios NPM package. We understand that is not a good practice
to follow as, when adding more API clients, we do not want to have
references to a 3rd party NPM packages spread throughout our
code. Imagine if we had built a huge code base with many
components and state modules and now wewanted to using
something like Fetch Api³¹ or another library insteaf of axios. We
would have to replace all the calls that use axios in our entire code
base. What we need to do is abstract the http client methods into
their own implementation that
wecanthenconsumefromourItemsApiClientandfuture API clients
implementations that we’ll be adding later. There are multiple ways
we could do this, but the most straigh-forward way is to create a
class that wraps our calls done with axios in one place. We’ll call
this the HttpClient class and here we’ll implement code that allow
us to perform http requests using axios for now. If later we have to
switch to a different NPM library or use the Fetch API etc, we’ll jsut
need to update the code without our HttpClient. Ass long as we do
not change the signature of our HttpClient methods, everything
should still work as before without having to change the code that
consumes our HttpClient throughout our application. Here I will
show you how this pattern works by offering both an
implementation that uses axios and one that uses the browser Fetch
API. Then in the net chapter will drive which client we use through
the app configuration. HttpClient Interfaces and Models Create the
directory src/http-client/models. Within this directory, create the
following files • Constants.ts • HttpRequestParams.interface.ts •
UrlUtils.ts • HttpClient.interface.ts • HttpClient.axios.ts
³¹https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
Chapter 8- Enhance the Api Client 107 • HttpClient.fetch.ts •
index.ts Your directory structure will look like this: Following is the
the description and code for each of the files. Constants.ts Within
the Constants.ts file, we’ll add an enum representing the type of
http request we want our HttpClient to execute. For now we just add
the 4 most common http verbs: get/post/put/delete: Chapter 8-
Enhance the Api Client 108 // file:
src/http-client/models/HttpRequestType.ts /** * @name
HttpRequestType * @description * The type of http request we need
to execute in our HttpClient request method */ export const enum
HttpRequestType { get, post, put, delete, patch } ... We’ll also add
two readonly objects to avoid using hard-coded strings later: // file:
src/http-client/models/HttpRequestType.ts ... // http content types
export const HttpContentTypes = Object.freeze({ applicationJson:
'application/json', formUrlEncoded: 'application/x-www-form-
urlencoded;charset=UTF-8' }) // constant for http request methods
names export const HttpRequestMethods = Object.freeze({ get:
'GET', post: 'POST', put: 'PUT', delete: 'DELETE', patch: 'PATCH' })
HttpRequestParams.interface.ts
TheHttpRequestParamsInterfacewillallowustopassparameterstotheH
ttpClientrequest method. These are things like the type of request
(GET/POST/etc), the API endpoint, an Chapter 8- Enhance the Api
Client 109 optional payload (if POST or PUT), and a flag that
indicates if the request must include an authentication token. // file:
src/http-client/models/HttpRequestParams.interface.ts import
{ HttpRequestType } from './Constants' /** * @name
HttpRequestParamsInterface * @description * Interface represents
an object we'll use to pass arguments into our HttpClient re\ quest
method. * This allow us to specify the type of request we want to
execute, the end-point ur\ l, * if the request should include an
authentication token, and an optional payload (i\ f POST or PUT for
example) */ export interface HttpRequestParamsInterface

{ requestType: HttpRequestType endpoint: string requiresToken:


boolean headers?: { [key: string]: string } payload?: P mockDelay?:
number } NOTE: With **P ** we are trying to enfore more type-
checking with TypeScript when we’ll consume this, at the same time
we need to add as P = void as this is not always required. UrlUtils.ts
This mainly contains an helper to dynamically build urls with
parameters: Chapter 8- Enhance the Api Client 110 // file: src/http-
client/models/UrlUtils.ts export interface UrlUtilsInterface
{ getFullUrlWithParams(baseUrl: string, params: { [key: string]:
number | string }):\ string } export const UrlUtils: UrlUtilsInterface
= { /** * @name getFullUrlWithParams * @description Returns the
full formatted url for an API end-point * by replacing parameters
place holder with the actual values. * @param baseUrl The base API
end-point witht he params placeholders like {projec\ tId} * @param
params The request params object with the key/value entries for
each par\ ameter * @returns The fully formatted API end-point url
with the actual parameter values */ getFullUrlWithParams: (baseUrl:
string, params: { [key: string]: number | string }\ ): string =>
{ const keys: string[] = Object.keys(params || {}) if ((baseUrl ||
'').indexOf('[') ===-1 || keys.length === 0) { return baseUrl } let
fullUrl = baseUrl keys.forEach((key) => { fullUrl = fullUrl.replace(`[$
{key}]`, (params[key] || 'null').toString()) }) return fullUrl } } Note:
you could alternatively implement getFullUrlWithParams using the
JavaScript built-in Url. HttpClient.interface.ts The
HttpClientInterface is the interface that defines the methods that
the HttpClient will
havetoimplement.Therewillbeonlyonemethodcalledrequestwhichcan
executedifferent types of http request based on the parameters
argument provided, and returns a Promise³² with the results (if
any):
³²https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
/Global_Objects/Promise Chapter 8- Enhance the Api Client 111 //
files: src/http-client/models/HttpClient.interface.ts import
{ HttpRequestParamsInterface } from
'./HttpRequestParams.interface' /** * @name
HttpClientConfigInterface * @description * We'll drive the HttpClient
from configuration in later chapters. */ export interface
HttpClientConfigInterface { tokenKey: string clientType: string } /**
* @name HttpClientInterface * @description * Represents our
HttpClient. */ export interface HttpClientInterface { /** * @name
request * @description * A method that executes different types of
http requests (i.e. GET/POST/etc) * based on the parameters
argument. * The type R specify the type of the result returned * The
type P specify the type of payload if any * @returns A Promise as
the implementation of this method will be async. */
request(parameters: HttpRequestParamsInterface
): Promise } Note: in the code above the request method can take 2
generic types. The first one, R, define the type of the result/data
returned. The second, P, is optional and defines the type of the
payload (if any) passed with the parameters argument.
HttpClient.axios.ts The HttpClientAxios is the class that implements
our HttpClientInterface using axios. Since the code here is longer,
let me split in multiple parts. First the import section: Chapter 8-
Enhance the Api Client 112 // file:
src/http-client/models/HttpClient.axios.ts import axios,
{ AxiosRequestConfig, AxiosResponse } from 'axios' import
{ HttpRequestParamsInterface } from
'./HttpRequestParams.interface' import { HttpClientInterface,
HttpClientConfigInterface } from './HttpClient.interfa\ ce' import
{ HttpRequestType, HttpContentTypes } from './Constants' import
{ UrlUtils } from './UrlUtils' /** * @name HttpClientAxios *
@description * Wraps http client functionality to avoid directly using
a third party npm package\ like axios * and simplify replacement in
the future if such npm package would stop being devel\ oped or
other reasons */ export class HttpClientAxios implements
HttpClientInterface { constructor() { // OPTIONAL for now: Add
request interceptor to handle errors or other things fo\ r each
request in one place } /** * @name request * @description * A
method that executes different types of http requests (i.e.
GET/POST/etc) * based on the parameters argument. * The type R
specify the type of the result returned * The type P specify the type
of payload if any * @returns A Promise as the implementation of this
method will be async. */ async request(parameters:
HttpRequestParamsInterface

): Promise { // use destructuring to extract our parameters into local


variables const { requestType, endpoint, requiresToken, payload,
headers, mockDelay } = pa\ rameters // use helper to build the
fullUrl with request parameters derived from the payl\ Chapter 8-
Enhance the Api Client 113 oad const fullUrl =
UrlUtils.getFullUrlWithParams(endpoint, payload as any)
console.log('HttpClientAxios: fullUrl: ', fullUrl, payload) // set axios
options const options: AxiosRequestConfig = { headers: {},
maxRedirects: 0 } if (headers) { options.headers =
{ //...options.headers, ...headers } } // set headers Authorization if
(requiresToken && options.headers) { options.withCredentials =
true // optional: you could add coded here to set the Authorization
header with a b\ earer token // options.headers.Authorization =
`bearer ${ JwtHelpers.getJwtToken() }` } let result!: R try
{ switch(requestType) { // TODO: implement a case statement for
each request type default: { console.warn('HttpClientAxios: invalid
requestType argument or request typ\ e not implemented') } } }
catch (e) { console.error('HttpClientAxios: exception', e) throw
Error('HttpClientAxios: exception') } if ((mockDelay || 0) > 0)
{ Chapter 8- Enhance the Api Client 114 return new
Promise((resolve) => { setTimeout(() => { resolve(result) },
mockDelay) }) } return result } } Note how we added a constructor
placeholder, but not doing anything with it yet. Later, you could add
things like request interceptors within the contructor so you can log
or capture errors in one place. One more thing to notice is that we
are using a try/catch block and just log the error in the console, but
we are not gracefully rejecting the promise return by our request
method. You are welcome to enhance and improve this code as you
see fit based on your sepcific requirements. The implementation of
the request method starts by destructuring our request parameters,
creates the fullUrl, setting some axios options, optionally setting an
Authorization header (commented out for now, but to show how you
can do that if you need it), and a switch statement that will execute
the type of request we want. Let’s implement now the different type
of requests within each case block of our switch statement. The get
implementation: // file:
src/http-client/models/HttpClient.axios.ts ... // executes a get
request: case HttpRequestType.get: { const response = await
axios.get(fullUrl, options) result = response?.data as R break } ...
The post implementation: Chapter 8- Enhance the Api Client 115 //
file: src/http-client/models/HttpClient.axios.ts ... // executes a post
request: case HttpRequestType.post: { const response = await
axios.post(fullUrl, payload, options) result = response?.data as R
break } ... The put implementation: // file:
src/http-client/models/HttpClient.axios.ts ... // executes a put
request: case HttpRequestType.put: { const response = await
axios.put(fullUrl, payload, options) result = response?.data as R
break } ... The delete implementation: // file:
src/http-client/models/HttpClient.axios.ts ... // executes a delete
request: case HttpRequestType.delete: { const response = await
axios.delete(fullUrl, options) result = response?.data as R break } ...
The patch implementation: Chapter 8- Enhance the Api Client 116 //
file: src/http-client/models/HttpClient.axios.ts ... // executes a patch
request: case HttpRequestType.patch: { const response = await
axios.patch(fullUrl, payload, options) result = response?.data as R
break } ... HttpClient.fetch.ts
TheHttpClientFetchistheclassthatimplementsourHttpClientInterface
usingfetch.Since the code here is longer, let me split in multiple
parts. // file: src/http-client/models/HttpClient.fetch.ts import
{ HttpRequestParamsInterface } from
'./HttpRequestParams.interface' import { HttpClientInterface,
HttpClientConfigInterface } from './HttpClient.interfa\ ce' import
{ HttpRequestType, HttpRequestMethods, HttpContentTypes } from
'./Constants' import { UrlUtils } from './UrlUtils' /** * @name
HttpClientFetch * @description * Wraps http client functionality to
avoid directly using fetch * and simplify replacement in the future if
such npm package would stop being devel\ oped or other reasons */
export class HttpClientFetch implements HttpClientInterface
{ constructor() { // OPTIONAL for now: Add request interceptor to
handle errors or other things fo\ r each request in one place } /** *
@name request * @description Chapter 8- Enhance the Api Client
117 * A method that executes different types of http requests (i.e.
GET/POST/etc) * based on the parameters argument. * The type R
specify the type of the result returned * The type P specify the type
of payload if any * @returns A Promise as the implementation of this
method will be async. */ async request(parameters:
HttpRequestParamsInterface

): Promise { // use destructuring to extract our parameters into local


variables const { requestType, endpoint, requiresToken, payload,
headers, mockDelay } = pa\ rameters // use helper to build the
fullUrl with request parameters derived from the payl\ oad const
fullUrl = UrlUtils.getFullUrlWithParams(endpoint, payload as any)
console.log('HttpClientFetch: fullUrl: ', fullUrl, payload) // set fetch
options const options: RequestInit = { credentials: 'include',
redirect: 'follow', headers: {} } if (headers) { options.headers =
{ ...headers } } if (!options.headers?.hasOwnProperty('Content-
Type')) { // default to content-type json options.headers =
{ ...headers, 'Content-Type': HttpContentTypes.applicationJson } } //
set headers Authorization if (requiresToken && options.headers) { //
optional: you could add coded here to set the Authorization header
with a b\ earer token // options.headers.Authorization = `bearer $
{ JwtHelpers.getJwtToken() }` } Chapter 8- Enhance the Api Client
118 let result!: R // helper for checking if response is being
redirected (302) in fetch const checkRedirect = async (resp: any) =>
{ if (resp.redirected) { // if so, redirect to response url
document.location = resp.url return true } return false } try
{ switch (requestType) { // TODO: implement a case statement for
each request type default: { console.warn('HttpClientFetch: invalid
requestType argument or request typ\ e not implemented') } } }
catch (e) { //console.error('HttpClientFetch: exception', e) throw
Error('HttpClientFetch: exception') } if ((mockDelay || 0) > 0)
{ return new Promise((resolve) => { setTimeout(() =>
{ resolve(result) }, mockDelay) }) } return result } } The
implementation of the request method starts by destructuring our
request parameters, creates the fullUrl, setting some fetch options,
optionally setting an Authorization header (commented out for now,
but to show how you can do that if you need it), and a switch
Chapter 8- Enhance the Api Client 119 statement that will execute
the type of request we want. Let’s implement now the different type
of requests within each case block of our switch statement. The get
implementation: // file:
src/http-client/models/HttpClient.fetch.ts ... // executes a get
request: case HttpRequestType.get: { options.method =
HttpRequestMethods.get const response = (await fetch(fullUrl,
options)) as any const redirected = await checkRedirect(response) if
(!redirected) { result = (await response.json()) as R } break } ... The
post implementation: // file:
src/http-client/models/HttpClient.fetch.ts ... // executes a post
request: case HttpRequestType.post: { options.method =
HttpRequestMethods.post options.body = typeof payload ===
'string' ? payload : JSON.stringify(payload) const response = (await
fetch(fullUrl, options)) as any const redirected = await
checkRedirect(response) if (!redirected) { result = (await
response.json()) as R } break } ... The put implementation: Chapter
8- Enhance the Api Client 120 // file:
src/http-client/models/HttpClient.fetch.ts ... // executes a put
request: case HttpRequestType.put: { options.method =
HttpRequestMethods.put options.body = typeof payload ===
'string' ? payload : JSON.stringify(payload) const response = (await
fetch(fullUrl, options)) as any const redirected = await
checkRedirect(response) if (!redirected) { result = (await
response.json()) as R } break } ... The delete implementation: // file:
src/http-client/models/HttpClient.fetch.ts ... // executes a delete
request: case HttpRequestType.delete: { options.method =
HttpRequestMethods.delete const response = (await fetch(fullUrl,
options)) as any const redirected = await checkRedirect(response) if
(!redirected) { result = (await response.json()) as R } break } ... The
patch implementation: Chapter 8- Enhance the Api Client 121 // file:
src/http-client/models/HttpClient.fetch.ts ... // executes a patch
request: case HttpRequestType.patch: { options.method =
HttpRequestMethods.patch options.body = typeof payload ===
'string' ? payload : JSON.stringify(payload) const response = (await
fetch(fullUrl, options)) as any const redirected = await
checkRedirect(response) if (!redirected) { result = (await
response.json()) as R } break } ... http-client/models/index.ts (barrel
file) Inside the index file paste the following to export all the enums/
interfaces/models: // file: src/http-client/models/index.ts export *
from './Constants' export * from './HttpClient.axios' export * from
'./HttpClient.fetch' export * from './HttpClient.interface' export *
from './HttpRequestParams.interface' export * from './UrlUtils' http-
client/index.ts (client factory) Add another index file under src/http-
client: Chapter 8- Enhance the Api Client 122 This file contains the
export of a single instance of our HttpClient. This is what we’ll be
consuming in our API client. For now, we’ll create an instance of the
HttpClient.fetch implementation, but in later chapters we’ll drive
this from configuration (appConfig): // file: src/http-client/index.ts
import { HttpClientInterface } from './models/HttpClient.interface'
//import { appConfig } from '@/app-config' import
{ HttpClientAxios } from './models/HttpClient.axios' import
{ HttpClientFetch } from './models/HttpClient.fetch' // export all our
interfaces/models/enums export * from './models' let _httpClient:
HttpClientInterface | undefined = undefined // export out hook
export const useHttpClient = () => { if (!_httpClient) { // export
instance of HttpClientInterface const clientType = 'fetch' // const
clientType = config.httpClient.clientType // later will drive from conf\
ig // if you'd like to use axios, set "clientType": "axios" within the
config files\ httpClient section if (clientType === 'fetch')
{ _httpClient = new HttpClientFetch() } else if (clientType ===
'axios') { _httpClient = new HttpClientAxios() Chapter 8- Enhance
the Api Client 123 } } return _httpClient as HttpClientInterface }
UrlUtils Unit Tests Create the directory tests/unit/http-client
directory and add a new file called UrlU
tils.getFullUrlWithParams.test.ts with the following: // file:
src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts
import { UrlUtils } from '@/http-client' describe('UrlUtils:
getFullUrlWithParams', () => { it('should return fullUrl formatted as
expected with one param', () => { const endpoint = 'https://unit-
test-api/v1/domain/[catalogId]/[partId]' const params = { catalogId:
5346782, partId: 'abcde23' } const result =
UrlUtils.getFullUrlWithParams(endpoint, params)
expect('https://unit-test-api/v1/domain/5346782/abcde23').toEqual(r
esult) }) // test our component click event it('should return fullUrl
formatted as expected with multiple params', () => { const
endpoint = 'https://unit-test-api/v1/domain/[country]/[state]/[cityId]'
const params = { country: 'USA', state: 'NY', cityId: 'gtref345ytr' }
const result = UrlUtils.getFullUrlWithParams(endpoint, params)
expect('https://unit-test-api/v1/domain/USA/NY/gtref345ytr').toEqual
(result) }) }) Chapter 8- Enhance the Api Client 124 HttpClient: Unit
Tests We need to add unit tests against HttpClientAxios and
HttpClientFetch before we can re-factor the ItemApiClient code to
use it. HttpClientAxios tests Testing a successful ”get” response
Within the directory tests/unit/http-client directory create a sub-
directory called axios client and here and add a new file called
AxiosClient.request.get.test.ts. Within the file, paste the following
code: // file:
src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts
import axios from 'axios' import { HttpClientAxios,
HttpRequestType, HttpRequestParamsInterface } from '@/http\-
client' let mockRequestParams: HttpRequestParamsInterface =
{ requestType: HttpRequestType.get, endpoint:
'path/to/a/get/api/endpoint', requiresToken: false }
describe('HttpClient: axios-client: request: get', () => { const
httpClient = new HttpClientAxios() it('should execute get request
succesfully', () => { vitest .spyOn(axios,
'get') .mockImplementation(async () => Promise.resolve({ data:
`request completed: ${\ mockRequestParams.endpoint}` }))
httpClient .request(mockRequestParams) .then((response) => {
//console.debug('response:', response)
expect(response).toEqual(`request completed: $
{mockRequestParams.endpoint}`) }) .catch((error) => { Chapter 8-
Enhance the Api Client 125
console.info('AxiosClient.request.get.test.ts: error', error) }) }) ...
Testing an unsuccessful ”get” response Within the same file, add
the following code: // file:
src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts
... describe('HttpClient: axios-client: request: get', () => { ... it('get
should throw error on rejection', () => { vitest .spyOn(axios,
'get') .mockImplementation(async () => Promise.reject({ data:
`request completed: ${m\ ockRequestParams.endpoint}` }))
httpClient.request(mockRequestParams).catch((error) =>
{ expect(error).toBeDefined()
expect(error.toString()).toEqual('Error: HttpClientAxios:
exception') }) }) }) Testing a successful ”post” response Within the
directory tests/unit/http-client/axios-client directory and add a new
file called AxiosClient.request.post.test.ts. Within the file, paste the
following code: Chapter 8- Enhance the Api Client 126 // file:
src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.t
s import axios from 'axios' import { HttpClientAxios,
HttpRequestType, HttpRequestParamsInterface } from '@/http\-
client' let mockRequestParams: HttpRequestParamsInterface =
{ requestType: HttpRequestType.post, endpoint:
'path/to/a/post/api/endpoint', requiresToken: false, payload: {} }
type P = typeof mockRequestParams.payload describe('HttpClient:
axios-client: request: post', () => { const httpClient = new
HttpClientAxios() it('should execute post request succesfully', () =>
{ vitest .spyOn(axios, 'post') .mockImplementation(async () =>
Promise.resolve({ data: `request completed: ${\
mockRequestParams.endpoint}` }))
httpClient .request(mockRequestParams) .then((response) => {
//console.debug('response:', response)
expect(response).toEqual(`request completed: $
{mockRequestParams.endpoint}`) }) .catch((error) =>
{ console.info('AxiosClient.request.post.test.ts: post error', error) })
}) }) Note: you can keep adding more test in a similar way for the
rest of the request type like PUT/DELETE/etc Chapter 8- Enhance the
Api Client 127 HttpClientFetch tests Testing “get” responses Within
the directory tests/unit/http-client directory create a sub-directory
called fetch client and here and add a new file called
FetchClient.request.get.test.ts. Within the file, paste the following
code: // file:
src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts
import { HttpClientFetch, HttpRequestType,
HttpRequestParamsInterface, HttpRequestMe\ thods } from '@/http-
client' let mockRequestParams: HttpRequestParamsInterface =
{ requestType: HttpRequestType.get, endpoint:
'path/to/a/get/api/endpoint', requiresToken: false }
describe('HttpClient: axios-client: request: get', (done) => { const
httpClient = new HttpClientFetch() it('should execute get request
succesfully', async () => { // could not find an easy way to use
spyOn for fetch so overriding global.fetch // save original fetch const
unmockedFetch = global.fetch || (() => {}) global.fetch =
unmockedFetch const expectedResult = { result: `request
completed: ${mockRequestParams.endpoint}` }
vitest .spyOn(global, 'fetch') .mockImplementation(async () =>
Promise.resolve({ redirected: false, json: () =>
Promise.resolve(JSON.stringify(expectedResult)) } as any)) try
{ const response = await httpClient.request(mockRequestParams)
Chapter 8- Enhance the Api Client 128
expect(response).not.toBeNull()
expect(response).toEqual(expectedResult) } catch (error)
{ console.info('AxiosClient.request.get.test.ts: error', error) } //
restore globa.fetch global.fetch = unmockedFetch }) it('get should
throw error on rejection', () => { // could not find an easy way to use
spyOn for fetch so overriding global.fetch // save original fetch const
unmockedFetch = global.fetch || (() => {}) global.fetch =
unmockedFetch vitest .spyOn(global,
'fetch') .mockImplementation(async () => Promise.reject())
httpClient.request(mockRequestParams).catch((error) =>
{ expect(error).toBeDefined()
expect(error.toString()).toEqual('Error: HttpClientFetch:
exception') }) }) }) And so on, you can keep adding more unit tests
for each request type like you did for the axios-client. We can finally
change our ItemApiClient so it uses our newly implemented
HttpClient instead of axios. ItemsApiClientModel Update Open the
file src/api-client/models/items/ItemsApiClient.model.ts. Remove the
import axios line and replace it with an import for our HttpClient
instance and the HttpRequestParamsInterface: Chapter 8- Enhance
the Api Client 129 // file:
src/api-client/models/items/ItemsApiClient.model.ts import axios,
{ AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios' //
<-- \ remove this line import { useHttpClient, HttpRequestType,
HttpRequestParamsInterface } from '@/http-c\ lient' // <-- add this
line Then replace the fetchItems implementation with the
following: // file: src/api-client/models/items/ItemsApiClient.model.ts
... fetchItems(): Promise { const requestParameters:
HttpRequestParamsInterface = { requestType:
HttpRequestType.get, endpoint: this.endpoints.fetchItems,
requiresToken: false, mockDelay: this.mockDelay } return
useHttpClient().request(requestParameters) } ... This creates a
const variable to hold ourHttpRequestParamsInterfaceparameters,
and then return the call to httpClient.request (which is already a
Promise, so we do not have to do anything else here): Now, make
sure there are no errors in the terminal and the browser refreshes
correctly and load the data correctly. Chapter 8- Enhance the Api
Client 130 Chapter 8 Recap WhatWeLearned • Howto abstract an
http client into interfaces and models that are generic • How to
implement the HttpClientInterface into a model that encapsulate
the use of a 3rd party package in one place. We show this by
implement two different clients: HttpClientAxios and
HttpClientFetch. • How to use vitest.spyON for stubs so we can test
the HttpClient request method responses for different scenarios.
Observations • Wedid not write unit tests against the HttpClient
put/delete/patch methods • Wedid not write unit tests against the
ItemsApiClientModel Based on these observations, there are a few
improvements that you could make on your own: Improvements •
Add unit tests against the HttpClient put/delete/patch methods as
well • Add unit tests against the ItemsApiClient methods as well •
Experiment by adding another HttpClient implementation that uses
another Ajax library other than axios or fetch and then and modify
the file src/http-client/index.ts so that it instantiate this one instead
of the axios or fetch implementation. Then verify that the app still
run as expected. Chapter 9- App Configuration We need now to add
a way to configure our app through configuration files for different
environments (i.e. mock, beta, production, etc). NOTE: The code in
this chapter is not just specific to React. These concepts can be
applied to any front-end app (i.e. React/Vue/Svelte/Angular/etc). As
you recall from Chapter 7, we extended import.meta.env declaration
types (file src/vite env.d.ts) to include a new variable called
VITE_API_CLIENT. This currently drives the selection of the API client
at run time (mock or live). As you can imagine, as we add more
configuration, we might end adding a lot of new variables prefixed
with VITE_. This works, but can quickly become very hard to
manage, especially for large configurations that will drive many
settings. A better approach is to drive the entire configuration
through only one variable that we are going to call
VITE_APP_CONFIG. We’ll store all the settings in dedicated JSON
files. We’ll have one configuration file for each environment
(mock/beta/production/etc) and then load that dynamically at run-
time (or build time) based on our new VITE_APP_CONFIG
environment variable. vite-env.d.ts updates (or env.d.ts) Let’s start
by modifying the code within the Vite types declarations file.
Rename the current variable VITE_API_CLIENT to
VITE_APP_CONFIG: // file: src/vite-env.d.ts (or src/env.d.ts) ... // types
for Vite env variables: // (reference: https://vitejs.dev/guide/env-
and-mode.html#intellisense-for-typescrip\ t) interface
ImportMetaEnv { readonly VITE_APP_CONFIG: string // rename this
from VITE_API_CLIENT to VITE_APP_C\ ONFIG // more env variables...
Chapter 9- App Configuration 132 } interface ImportMeta { readonly
env: ImportMetaEnv } .env files updates Make sure to also update
each ‘.env’ file by renaming VITE_API_CLIENT to VITE_APP_
CONFIG: // file: src/.env.mock VITE_APP_CONFIG=mock // file:
src/.env.production VITE_APP_CONFIG=production Add also three
additional files, .env.jsonserver, .env.localapis and .env.beta: // file:
src/.env.jsonserver VITE_APP_CONFIG=jsonserver // file:
src/.env.localapis VITE_APP_CONFIG=localapis // file: src/.env.beta
VITE_APP_CONFIG=beta Note: remember that VITE read from
the .env files based on the–mode flag specified in the scripts
shortcut (within the package.json file). Here we did not add any
command for localapis or beta. But you could add things like start-
local or build-beta etc: Chapter 9- App Configuration 133 // file:
package.json ... "scripts": { "start": "npm run dev", "dev": "vite--
config vite.config.mock.ts--mode mock", "build": "tsc && vite build--
config vite.config.production.ts--mode production\ ", "build-mock":
"tsc && vite build--config vite.config.mock.ts--mode mock", "build-
beta": "tsc && vite build--config vite.config.production.ts--mode
beta"\ , /* you could add this */ "start-local": "vite--config
vite.config.production.ts--mode localapis", /* yo\ u could add this */
"preview": "vite preview--config vite.config.mock.ts--mode mock",
"test": "vitest run--config vite.config.mock.ts--mode mock", "test-
watch": "vitest watch--config vite.config.mock.ts--mode mock",
"test-coverage": "vitest run--coverage--config vite.config.mock.ts--
mode mock\ ", } ... Config Interface Create the directory
src/config/models/ and under this directory create a file called
Config.interface.ts. This contains the declaration for our config
interface. You will keep expanding this as you add more settings or
app domains (i.e. like Items), for now let’s just have the interface
contain four sections: • global: this will be for settings that span all
domains • httpClient: this is for things related to the HttpClient •
apiClient: this is for things related to the ApiClient • items: this is
for the Items domain settings (as we add more
functionality/components etc we will add more areas/domains
similar to this) For the items section, we’ll have only the
apiClientOptions child section for now. This will be of type
ItemsApiClientOptions. Here is the code for the
src/config/models/Config.interface.ts file: Chapter 9- App
Configuration 134 // file: src/config/models/Config.interface.ts import
{ ItemsApiClientOptions // NOTE: we'll create this a bit later } from
'@/api-client/models' export interface HttpClientConfigInterface
{ tokenKey: string clientType: string } /** * @Name ConfigInterface *
@description * Describes the structure of a configuration file */
export interface ConfigInterface { global: { // ... things that are not
specific to a single app domain version: number } httpClient:
HttpClientConfigInterface, apiClient: { type: string } items:
{ apiClientOptions: ItemsApiClientOptions } } Config files Now
create a sub-directory called config-files under this directory. The
full path for this will be src/config/config-files/ Inside this directory,
add 4 JSON files with the following names: • mock.json Chapter 9-
App Configuration 135 • jsonserver.json • localapis.json • beta.json
• production.json The content of each file will have to match what is
required by our ConfigInterface. In a little bit we’ll be also adding
some unit tests against this files to make sure they are as expected.
Here is the content of each file: mock.json // file: src/config/config-
files/mock.json { "global": { "version": 0.103 }, "httpClient":
{ "tokenKey": "myapp-token", "clientType": "fetch" }, "apiClient":
{ "type": "mock" }, "items": { "apiClientOptions": { "endpoints":
{ "fetchItems": "/static/mock-data/items/items.json" }, "mockDelay":
250 } } } jsonserver.json Chapter 9- App Configuration 136 // file:
src/config/config-files/jsonserver.json { "global": { "version": 0.1 },
"httpClient": { "tokenKey": "myapp-token", "clientType": "fetch" },
"apiClient": { "type": "live" }, "items": { "apiClientOptions":
{ "endpoints": { "fetchItems": "/jsonserver/items" }, "mockDelay":
0 } } } localapis.json // file: src/config/config-files/localapis.json
{ "global": { "version": 0.1 }, "httpClient": { "tokenKey": "myapp-
token", "clientType": "fetch" }, "apiClient": { "type": "live" },
Chapter 9- App Configuration 137 "items": { "apiClientOptions":
{ "endpoints": { "fetchItems": "http://api.localhost:4111/items" },
"mockDelay": 0 } } } beta.json // file:
src/config/config-files/beta.json { "global": { "version": 0.1 },
"httpClient": { "tokenKey": "myapp-token", "clientType": "fetch" },
"apiClient": { "type": "live" }, "items": { "apiClientOptions":
{ "endpoints": { "fetchItems": "/path/to/your/real/BETA/api/and-
point" }, "mockDelay": 0 } } } production.json Chapter 9- App
Configuration 138 // file: src/config/config-files/production.json
{ "global": { "version": 0.1 }, "httpClient": { "tokenKey": "myapp-
token", "clientType": "fetch" }, "apiClient": { "type": "live" },
"items": { "apiClientOptions": { "endpoints": { "fetchItems":
"/path/to/your/real/PRODUCTION/api/and-point" }, "mockDelay":
0 } } } tsconfig.json updates In the next section, we’ll be loading
the individual config files through import statements. In order to
enable this in TypeScript, we have to modify the tsconfig.json file
located in the root of your project. We need to add option
resolveJsonModule with true to the compilerOptions section:
Chapter 9- App Configuration 139 { "compilerOptions": { ...
"resolveJsonModule": true, /* this allows to import .json file as if
they were .\ ts files: using to load config files */ } ...
NOTE:yourtsconfig.json might already have the resolveJsonModule
flag, if so just make sure that is set to true. Config files map Within
the directory src/config/ add a file called config-files-map.ts. Here
we just import a reference to each of the configuration JSON files
and create either a strategy pattern or a JavaScript Map that
contains a map to our files by environment key (here we are
showing this with Map): // file: src/config/config-files-map.ts //
import a reference to our Config interface: import
{ ConfigInterface } from './models/Config.interface' // individual
environments configs: import configMock from
'./config-files/mock.json' import configJsonServer from
'./config-files/jsonserver.json' import configLocal from
'./config-files/localapis.json' import configBeta from
'./config-files/beta.json' import configProduction from
'./config-files/production.json' // example with javascript Map()
export const configFilesMap: Map = new Map([ ['mock', configMock],
['jsonserver', configJsonServer], ['localapis', configLocal], ['beta',
configBeta], ['production', configProduction] ]) Chapter 9- App
Configuration 140 Config provider File utils.ts Addanewfile called
utils.ts under src/config. Here we implement an helper function
called getAppConfigKey that will return the value of our
VITE_APP_CONFIG environment variable. // file: src/config/utils.ts //
helper to read the value of REACT_APP_CONFIG (or
VITE_APP_CONFIG if using vite) export function getAppConfigKey() {
// if using webpack: // let env: string = 'mock' // // @ts-ignore // if
(process.env && process.env.REACT_APP_CONFIG) { // // @ts-
ignore // env = process.env.REACT_APP_CONFIG.trim() // } // return
env // Note: Vite uses import.meta.env (reference:
https://vitejs.dev/guide/env-and-mo\ de.html) // optional: you can
console.log the content of import.meta.env to inspect its val\ ues
like this: console.log('import.meta.env',
JSON.stringify(import.meta.env)) // @ts-ignore return
(import.meta.env.VITE_APP_CONFIG || '').trim() } Note that by
wrapping this in one place, we could easily re-use this code in a
project created with webpack. Since that uses process.env instead
of Vite’s import.meta.env we just need to uncomment the related
code above and comment out the one that uses import.meta.env.
File index.ts (config provider) Still within src/config directory, add
another file called index.ts. Here we’ll be consuming the JSON file
that matches the environment specified by the current
VITE_APP_CONFIG value. Let’s start by importing a reference to
ConfigInterface, configFilesMap, and our helper getAppConfigKey:
Chapter 9- App Configuration 141 // file: src/config/index.ts // returns
appropriate config based on env VITE_APP_CONFIG // import a
reference to our Config interface: import { ConfigInterface } from
'./models/Config.interface' // import reference to configFilesMap
import { configFilesMap } from './config-files-map' // import
reference to our getAppConfigKey helper function import
{ getAppConfigKey } from './utils' ... Then add a check and throw
and error if our map does not contain an entry for the current
environment key: // file: src/config/index.ts ... if (!
configFilesMap.has(getAppConfigKey())) { throw Error(`Could not
find config for VITE_APP_CONFIG key "${ getAppConfigKey() }\
"`) } ... Finally we export an instance of our ConfigInterface called
config: // file: src/config/index.ts ... export const config =
configFilesMap.get(getAppConfigKey()) as ConfigInterface Here is
the entire content of src/config/index.ts: Chapter 9- App
Configuration 142 // file: src/config/index.ts // returns appropriate
config based on env VITE_APP_CONFIG // import a reference to our
Config interface: import { ConfigInterface } from
'./models/Config.interface' // import reference to configFilesMap
import { configFilesMap } from './config-files-map' // import
reference to our getAppConfigKey helper function import
{ getAppConfigKey } from './utils' // optional: you can console.log
the content of import.meta.env to inspect its value:
console.log(`------ env---- "${getAppConfigKey()}"`) if (!
configFilesMap.has(getAppConfigKey())) { throw Error(`Could not
find config for VITE_APP_CONFIG key "${getAppConfigKey()}"`) }
export const config = configFilesMap.get(getAppConfigKey()) as
ConfigInterface Unit Tests Le’s now write a few unit tests to validate
that our config is being set as expected. This will also validate that
the config JSON files contains the expected data structure. Unit
Tests against configsMap Create directory tests/unit/config and add
a new file called Config.configsMap.spec.ts. Here we’ll be testing
that our configsMap instance contains at least one entry for each
environment, as expected: Chapter 9- App Configuration 143 // file:
src/tests/unit/config/config-files-map.test.ts import { configFilesMap
} from '@/config/config-files-map' describe('configFilesMap', () =>
{ it('instance should have "mock" key', () =>
{ expect(configFilesMap.has('mock')).toBe(true) }) it('instance
should have "jsonserver" key', () =>
{ expect(configFilesMap.has('jsonserver')).toBe(true) }) it('instance
should have "localapis" key', () =>
{ expect(configFilesMap.has('localapis')).toBe(true) }) it('instance
should have "beta" key', () =>
{ expect(configFilesMap.has('beta')).toBe(true) }) it('instance
should have "production" key', () =>
{ expect(configFilesMap.has('production')).toBe(true) }) } Unit
Tests against Config instances by environment Note: if using Jest,
we cannot just write unit tests against the config instance already
created in the src/config/index.ts file because Jest will throw the
following error: SyntaxError: Cannot use 'import.meta' outside a
module Note: Jest does not understand VITE’s import.meta.dev our
of the box. There are discussion on the web if you google this and
some people are using plugins or specific babel config etc. Here will
simply do not test the config instance from the src/config/index.ts
file but create a new one in our unit tests Vitest does not have this
issue but I thought it was worth mentioning it. To be safe in either
case, we will write our unit tests by creating an instance of ConfigIn
terface within the unit tests. We can easily do this by specifying the
environment key as a hard-coded string to the map.get method, i.e.
configFilesMap.get('mock'). Chapter 9- App Configuration 144 Tests
config.mock.test.ts Still under directory tests/unit/config add
another file called config.mock.test.ts with the following code: //
file: src/tests/unit/config/config.mock.test.ts // import the Config
interface import { ConfigInterface } from
'@/config/models/Config.interface' // import a reference to the
confiFilesMap import { configFilesMap } from '@/config/config-files-
map' describe('config: mock', () => { const config: ConfigInterface =
configFilesMap.get('mock') as ConfigInterface it('instance should
have "global" section', () =>
{ expect(config).toHaveProperty('global') }) it('instance should have
"httpClient" section', () =>
{ expect(config).toHaveProperty('httpClient') }) it('instance should
have "items" section', () =>
{ expect(config).toHaveProperty('items') }) describe('global', () =>
{ const section = config.global it('section should have "version"
property', () => { expect(section).toHaveProperty('version')
expect(typeof section.version).toBe('number')
expect(section.version).toBeGreaterThan(0) }) })
describe('httpClient', () => { const section = config.httpClient
it('section should have "tokenKey" property', () =>
{ expect(section).toHaveProperty('tokenKey') }) it('section should
have "clientType" property', () => { Chapter 9- App Configuration
145 expect(section).toHaveProperty('clientType') }) })
describe('apiClient', () => { const section = config.apiClient
it('section should have "type" property', () =>
{ expect(section).toHaveProperty('type') }) }) describe('items', ()
=> { const section = config.items it('section should have
"apiClientOptions" property', () =>
{ expect(section).toHaveProperty('apiClientOptions') })
describe('apiClientOptions', () => { const apiClientOptions =
section.apiClientOptions describe('endpoints', () => { const
endpoints = apiClientOptions.endpoints it('section should have
"fetchItems" property', () =>
{ expect(endpoints).toHaveProperty('fetchItems') // verify that
fetchItems url is a string and has a reasonable length expect(typeof
endpoints.fetchItems).toBe('string')
expect(endpoints.fetchItems.length).toBeGreaterThan(10) }) }) }) }
) }) Run the unit tests with npm run test: and verify all succeed:
Chapter 9- App Configuration 146 // terminal output: ... Test Files 8
passed (8) Tests 25 passed (24) Time 1.18s (in thread 77ms,
1537.16%) ... Please keep adding additional unit tests for each
environment (i.e. config.jsonserver.test.ts, config.production.test.ts
etc). HttpClient code updates file src/http-client/index.ts Now we
need to update the file src/http-client/index.ts and remove the hard-
coded value for the clientType variable. We’ll be instead reading the
value from the config instance (config.httpClient.type): // file:
src/http-client/index.ts import { HttpClientInterface } from
'./models/HttpClient.interface' import { config } from '@/config' // <--
uncomment (or add) this line import { HttpClientAxios } from
'./models/HttpClient.axios' import { HttpClientFetch } from
'./models/HttpClient.fetch' // export all our interfaces/models/enums
export * from './models' let _httpClient: HttpClientInterface |
undefined = undefined // export out hook export const useHttpClient
= () => { if (!_httpClient) { // export instance of HttpClientInterface
const clientType = 'fetch' // <-- uncomment this line const
clientType: string = 'fetch' // <-- remove this line Chapter 9- App
Configuration 147 // if you'd like to use axios, set "clientType":
"axios" within the config files\ (section "httpClient") if (clientType
=== 'fetch') { _httpClient = new HttpClientFetch() } else if
(clientType === 'axios') { _httpClient = new HttpClientAxios() } }
return _httpClient as HttpClientInterface } Api Client code updates
file src/api-client/index.ts Update also the file src/api-client/index.ts
and remove the code currently using the previous env variable and
replace it by consuming our value from the config instance
(config.apiClient.type). Here is the updated code: // file: src/api-
client/index.ts import { ApiClientInterface } from './models' import {
apiMockClient } from './mock' import { apiLiveClient } from './live'
import { config } from '@/config' // return either the live or the mock
client let apiClient: ApiClientInterface if (config.apiClient.type ===
'live') { // this time we just read our config.apiClie\ nt.type apiClient
= apiLiveClient } else { // default is always apiMockClient apiClient
= apiMockClient } export { apiClient } Chapter 9- App Configuration
148 file src/api-client/mock/items/index.ts
Updatethecodethatreturns the mockItemsAPIclientinstance to use
theapiClientOptions from the config: // file:
src/api-client/mock/items/index.ts import { config } from '@/config' //
<-- add this line import { ItemsApiClientInterface,
ItemsApiClientModel } from '../../models/items' // remove this
block: // const options: ItemsApiClientOptions = { // mockDelay: 250,
// endpoints: { // fetchItems: '/static/mock-data/items/items.json' // }
// } // instantiate the ItemsApiClient pointing at the url that returns
static json mock \ data const itemsApiClient: ItemsApiClientInterface
= new ItemsApiClientModel(config.items\ .apiClientOptions) // <--
this time we'll pass the options from the config // export our
instance export { itemsApiClient } file
src/api-client/live/items/index.ts Similarly, update the code that
returns the live Items API client instance to use the apiClientOptions
from the config: Chapter 9- App Configuration 149 // file: src/api-
client/live/items/index.ts // import a reference to the app config
import { config } from '@/config' import { ItemsApiClientInterface,
ItemsApiClientModel } from '../../models' // instantiate the
ItemsApiClient pointing at the url that returns static json live \ data
const itemsApiClient: ItemsApiClientInterface = new
ItemsApiClientModel(config.items\ .apiClientOptions) // export our
instance export { itemsApiClient } IMPORTANT: At this point, thanks
to the new way of driving things through the config, the
codeinbothfilessrc/api-client/live/items/index.tsandsrc/api-client/
mock/items/index.ts is basically identical. In later chapters we will
simplify and reduce the amount of code we initially created to serve
either mock or live data. But for now, we’ll keep the duplicated code
to avoid making this chapter too long. Now make sure you run all
the unit tests again, then serve the app again to make sure all
compiles and works as before. Chapter 9- App Configuration 150
Chapter 9 Recap WhatWeLearned • Welearned howtousestatic JSON
files to have multiple configuration settings, one for each
environment • How to dynamically return the appropriate config file
based on the new environment variable VITE_APP_CONFIG •
HowtoaddoptionresolveJsonModuletothetheTypeScripttsconfig.jsonfi
le,section compilerOptions to allow importing static JSON files
through import statement • Howto write unit tests against our
configuration code Observations • Fornowourconfigurationis pretty
small, but might grow larger as the application itself grows and we
need to add more configurable options. • Wedid not write unit tests
again each config file like we did for config.mock.test.ts
Improvements •
Goingforwardwe’llbeexpandingtheconfigurationaswekeepgrowingou
rapplication components and logic. • Youcanwriteadditional unit
tests similar to config.mock.test.ts for beta and production as well
(i.e. config.beta.test.ts, config.mock.production.ts, etc) Chapter 10-
Localization and Internationalization- Language Localization
“Localization refers to the adaptation of a product, application or
document content to meet the language, cultural and other
requirements of a specific target market (a locale)…” “…
Internationalization is the design and development of a product,
application or document content that enables easy localization for
target audiences that vary in culture, region, or language”³³ NOTE:
This chapter applies to you only if the application you are working
on will be used or marketed to multiple countries and it is desired to
present labels in the local language, as well as date/number formats
in the local culture. Most modern applications that target multiple
countries or cultures are architected in a way that is easy to present
the UI in different languages and also present values like numbers
or dates formatted as expected by the culture specific to that
country (hence, localized). In this book we’ll first leverage plugins
that allows us to present labels in different languages (i18next,
react-i18next) and later we’ll add also a custom plugin based on the
Intl API (supported by most modern browsers) to provide for
numbers/date formatting functionality based on different locales
(cultures). Plugins: i18next, react-i18next There are many JavaScript
libraries out there that simplify localization of a frontend app, but
the most widely used is the i18n library. The organization i18next³⁴
main tains a very nice React plugin called react-i18next, which is
published on NPM here https://www.npmjs.com/package/react-
i18next³⁵ In this book we’ll be creating an hook that wraps around
react-i18next an additional code. This will allow us to avoid code
cluttering and greatly simplify how we localize our components in
our React application.
³³https://www.w3.org/International/questions/qa-i18n
³⁴https://github.com/i18next ³⁵https://github.com/i18next/react-
i18next Chapter 10- Localization and Internationalization- Language
Localization 152 Let’s start by first adding the i18next and react-
i18next NPM packages to our application. Weneed to use the
command npm install-save i18next react-i18next. Weneedtousethe-
saveoptionaswewantthistobesavedaspartoftheapp”dependencies”
in the package.json: npm install--save i18next react-i18next Now,
before we proceed creating our boostrapping code for i18n and our
useLocalization hook, let’s first make a few changes to our
application configuration. Config updates ConfigInterface
Wewillbeintroducingaconceptofversioningheretodynamicallydrivedi
fferentversionsof data, introduce/retire views and components
overtime, or expire cached data on the browser. You will see this in
action first shortly when we’ll use to expire our translation data
stored in the browser cache. Let’s add a field called version to our
global section (note: we might have added this already in the one of
previous chapters): // file: src/config/models/Config.interface.ts ...
export interface ConfigInterface { global: { version: number // add
this line } ... Let’s also add a new section called localization like
this: Chapter 10- Localization and Internationalization- Language
Localization 153 // file: src/config/models/Config.interface.ts ...
export interface ConfigInterface { global: { version: number } ... //
add this block: localization: { apiClientOptions:
LocalizationApiClientOptions locales: { key: string, isDefault:
boolean }[] localStorageCache: { enabled: boolean,
expirationInMinutes: number } } ... Here we reference a
apiClientOptions. We don’t have this yet. We’ll add a new API client
module called localization shortly. First, let’s finish updating the
code related to the configuration. file mock.json Let’s add the data
we need in the src/config/files/mock.json as per our ConfigInterface
updates: // file: src/config/config-files/mock.json { "global":
{ "version": 0.1 // add this line }, ... // begin: add the localization
section "localization": { "apiClientOptions": { "endpoints": { Chapter
10- Localization and Internationalization- Language Localization 154
"fetchTranslation":
"/static/mock-data/localization/[namespace]/[key].json" },
"mockDelay": 250 }, "locales": [ // each of this objects represent a
locale available in our app { "key": "en-US", "isDefault": true },
{ "key": "it-IT", "isDefault": false }, { "key": "fr-FR", "isDefault":
false }, { "key": "es-ES", "isDefault": false } ], "localStorageCache":
{ // these are settings we'll use to cache JSON locale translation
data into loc\ aleStorage "enabled": true, "expirationInMinutes":
60 } } // end: add the localization section } Please feel free to also
update the beta.json/production.json/localapis.json files as well, and
possibly add unit tests to validate your changes. Note that we have
also an array called locales
whichholdalistofobjectthatrepresenteachofthelocalesavailableinorap
plication. We also have a section caled localeStorageCache that
we’ll use to drive how we cache the locale translation JSON data into
the browser localStorage.³⁶ Translation JSON data Note that for the
fetchTranslation end-point, we’ll use two parameters: [namespace]
and [key]. We’ll create the files under
/public/static/mock-data/localization/. For
the[namespace]parameterwe’llalwaysuse‘translation’inourcase,sog
oaheadandcre ate
asub-directorycalledtranslationatthepath/public/static/mock-data/lo
calization/translation. Then, add 4 files: • en-US.json • es-ES.json •
fr-FR.json
³⁶https://developer.mozilla.org/en-US/docs/Web/API/Window/localSto
rage Chapter 10- Localization and Internationalization- Language
Localization 155 • it-IT.json Here is the content for the en-US.json
one: // file: public/static/mock-data/localization/translation/en-
US.json { "locale.selector.en-US": "English", "locale.selector.it-IT":
"Italian", "locale.selector.fr-FR": "French", "locale.selector.es-ES":
"Spanish", "home.welcome": "Welcome: this message is localized in
English", "navigation.home": "Home", "navigation.about": "About",
"items.list.header": "My Items" } Here is the content for the es-
ES.json one: // file:
public/static/mock-data/localization/translation/es-ES.json
{ "locale.selector.en-US": "Inglés", "locale.selector.it-IT": "Italiano",
"locale.selector.fr-FR": "Francés", "locale.selector.es-ES": "Español",
"home.welcome": "Bienvenido: this message is localized in Spanish",
"navigation.home": "Inicio", "navigation.about": "Acerca de",
"items.list.header": "Mis cosas" } Here is the content for the fr-
FR.json one: Chapter 10- Localization and Internationalization-
Language Localization 156 // file:
public/static/mock-data/localization/translation/fr-FR.json
{ "locale.selector.en-US": "Anglais", "locale.selector.it-IT": "Italien",
"locale.selector.fr-FR": "Français", "locale.selector.es-ES":
"Espagnol", "home.welcome": "Bienvenue: this message is localized
in French", "navigation.home": "Accueil", "navigation.about": "À
propos de nous", "items.list.header": "Mes articles" } Here is the
content for the it-IT.json one: // file:
public/static/mock-data/localization/translation/it-IT.json
{ "locale.selector.en-US": "Inglese", "locale.selector.it-IT": "Italiano",
"locale.selector.fr-FR": "Francese", "locale.selector.es-ES":
"Spagnolo", "home.welcome": "Benvenuti: this message is localized
in Italian", "navigation.home": "Home", "navigation.about": "Chi
Siamo", "items.list.header": "I miei articoli" } Wenowhave to add a
new API client module for loading our localization data. API Client
updates Create the directory src/api-client/models/localization. Here
we’ll create the interfaces and model for our localization API
module. Add the following 4 files: •
LocalizationApiClient.interface.ts Chapter 10- Localization and
Internationalization- Language Localization 157 •
LocalizationApiClientOptions.interface.ts •
LocalizationApiClient.model.ts • index.ts file
LocalizationApiClient.interface.ts Our localization API client will
exposes one method called fetchTranslation: // file:
src/api-client/models/localization/LocalizationApiClient.interface.ts
/** * @Name LocalizationApiClientInterface * @description *
Interface for the Localization api client module */ export interface
LocalizationApiClientInterface { fetchTranslation: (namespace:
string, key: string) => Promise } file
LocalizationApiClientOptions.interface.ts Here we have the
itnerfaces for the API client configuration: // file:
src/api-client/models/localization/LocalizationApiClientOptions.interf
ace.ts export interface LocalizationApiClientEndpoints
{ fetchTranslation: string } /** * @Name
LocalizationApiClientOptions * @description * Interface for the
Localization api client options (includes endpoints used to avo\ id
hard-coded strings) */ export interface LocalizationApiClientOptions
{ mockDelay?: number endpoints: LocalizationApiClientEndpoints }
Chapter 10- Localization and Internationalization- Language
Localization 158 file LocalizationApiClient.model.ts Here is the
implementation of our localization API client: // file:
src/api-client/models/localization/LocalizationApiClient.model.ts
import { useHttpClient, HttpRequestParamsInterface,
HttpRequestType } from '@/http-c\ lient' import
{ LocalizationApiClientOptions, LocalizationApiClientEndpoints }
from './Loca\ lizationApiClientOptions.interface' import
{ LocalizationApiClientInterface } from
'./LocalizationApiClient.interface' /** * @Name
LocalizationApiClientModel * @description * Implements the
LocalizationApiClientInterface interface */ export class
LocalizationApiClientModel implements
LocalizationApiClientInterface { private readonly endpoints!:
LocalizationApiClientEndpoints private readonly mockDelay: number
= 0 constructor(options: LocalizationApiClientOptions)
{ this.endpoints = options.endpoints if (options.mockDelay)
{ this.mockDelay = options.mockDelay } }
fetchTranslation(namespace: string, key: string): Promise { const
requestParameters: HttpRequestParamsInterface = { requestType:
HttpRequestType.get, endpoint: this.endpoints.fetchTranslation,
requiresToken: false, payload: { namespace, key } as any,
mockDelay: this.mockDelay } return
useHttpClient().request(requestParameters) Chapter 10-
Localization and Internationalization- Language Localization 159 } }
file src/api-client/models/localization/index.ts Just a barrel file: // file:
src/api-client/models/localization/index.ts export * from
'./LocalizationApiClientOptions.interface' export * from
'./LocalizationApiClient.interface' export * from
'./LocalizationApiClient.model' Updates to ApiClient.interface.ts
Import and add our localization module: // file:
src/api-client/models/ApiClient.interface.ts import
{ LocalizationApiClientInterface } from './localization' import
{ ItemsApiClientInterface } from './items' /** * @Name
ApiClientInterface * @description * Interface wraps all api client
modules into one places for keeping code organized. */ export
interface ApiClientInterface { localization:
LocalizationApiClientInterface items: ItemsApiClientInterface }
Updates to the main models barrel file (api-client/models/index.ts)
Chapter 10- Localization and Internationalization- Language
Localization 160 // file: src/api-client/models/index.ts export * from
'./ApiClient.interface' export * from './localization' export * from
'./items' Updates to ApiClient instances localization mock instance
(api-client/mock/localization/index.ts) Create the file
api-client/mock/localization/index.ts with this code: // file: src/api-
client/mock/localization/index.ts // import a reference to the app
config import { config } from '@/config' import
{ LocalizationApiClientInterface, LocalizationApiClientModel } from
'../../mo\ dels' // instantiate the LocalizationApiClient pointing at the
url that returns static jso\ n mock data const localizationApiClient:
LocalizationApiClientInterface = new LocalizationApiCli\
entModel(config.localization.apiClientOptions) // export our instance
export { localizationApiClient } mockinstance
(api-client/mock/index.ts) Update the code within the
api-client/mock/index.ts file like this: Chapter 10- Localization and
Internationalization- Language Localization 161 // file: src/api-
client/mock/index.ts import { ApiClientInterface } from '../models' //
import module instances import { localizationApiClient } from
'./localization' import { itemsApiClient } from './items' // create an
instance of our main ApiClient that wraps the mock child clients
const apiMockClient: ApiClientInterface = { localization:
localizationApiClient, items: itemsApiClient } // export our instance
export { apiMockClient } live instance (api-client/live/index.ts)
Update the code within the api-client/live/index.ts file like this : //
file: src/api-client/live/index.ts // import a reference to the app
config import { config } from '@/config' import { ApiClientInterface,
LocalizationApiClientModel, ItemsApiClientModel } from
'../models' // create an instance of our main ApiClient that wraps the
live child clients const apiLiveClient: ApiClientInterface =
{ localization: new
LocalizationApiClientModel(config.localization.apiClientOptions),
items: new ItemsApiClientModel(config.items.apiClientOptions) } //
export our instance export { apiLiveClient } Note: for the live
instance going forward we’ll just initialize the client modules within
this file so you can go ahead and delete theapi-client/live/items sub-
directory. Chapter 10- Localization and Internationalization-
Language Localization 162 i18n initialization and useLocalization
hook Create the directory src/localization. Inside the localization
folder we’ll create the following files: • I18n.init.ts •
useLocalization.ts • index.ts file i18n.init.ts For the i18n.init.ts code,
we’ll start by importing a few types and utils from both i18next and
react-i18next. Also import reference to our config and apiClient: //
file: src/localization/i18n.init.ts import { initReactI18next } from
'react-i18next' import i18n, { BackendModule, Services, TOptions,
InitOptions, ReadCallback } from 'i18next' import { config } from
'../config' import { apiClient } from '../api-client' ... Let’s get a
reference to the localeStorageCache configuration: Chapter 10-
Localization and Internationalization- Language Localization 163 //
file: src/localization/i18n.init.ts ... // get reference to out localization
config const localStorageConfig =
config.localization.localStorageCache ... Create a constant that we’ll
use as the key for saving or retrieving information about the user
preferred locale from the browser localStorage: // file:
src/localization/i18n.init.ts ... // key that will use to save the user
preferred locale id export const userPreferredLocaleStorageKey =
'user-lcid' ... Add two helper methods used to retrieve or save the
user preferred locale id to localStorage, called
getUserPreferredLocale and setUserPreferredLocale: // file:
src/localization/i18n.init.ts ... // helper method to retrieve the user
preferred locale from localStorage export const
getUserPreferredLocale = () => { // get a reference from the
available locales array from our config const availableLocales =
config.localization.locales // try to retrive from local storage if they
have one saved const preferredLocale =
localStorage.getItem(userPreferredLocaleStorageKey) if (!
preferredLocale) { // if not, use the default locale from config const
defaultLocale = availableLocales.find(o => o.isDefault)?.key return
defaultLocale } return preferredLocale } // helper to save the user
preferred locale to localStorage export const
setUserPreferredLocale = (lcid: string) =>
{ localStorage.setItem(userPreferredLocaleStorageKey, lcid)
Chapter 10- Localization and Internationalization- Language
Localization 164 } ... Note that in getUserPreferredLocale we return
the default locale from config if there is not preferred locale in
localStorage yet
AddanhelpercalledgetLocaleDatathatwillhelpusloadJSONtranslationd
ataforaspecific locale from an API and cache it into localStorage (so
subsequent calls to this method will retrieve the JSON data from
cache): // file: src/localization/i18n.init.ts ... // helper to get JSON
locale translation data const getLocaleData = async (namespace:
string, lcid: string): Promise

You might also like