LargeScaleWebApps - Copy - Copy (2)
LargeScaleWebApps - Copy - Copy (2)
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}
)}
Items:
{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
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}
...
Items:
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:
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
Items:
o t\ his.handleItemClick(item)}>{item.name}
)}
Items:
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 (
); } 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
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}
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
Items:
o this.handleItemClick(item)}>{\ item.name}
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 */} //
) } 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 (
... 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 =
} 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 ...