https://docs.google.com/presentation/d/1w8HRmAK2HB5PuwOB0HdFeGbvntsvM3-rvHdNOPZ3lBs
Install all dependencies and link executables to child projects with npm i
in the exercises
folder.
The main purpose of this exercise is to try React and its stateful components implemented with React Hooks.
00-init
Header
and UserList
)Location: src/modules/root/components/header.tsx
HeaderProps:
{
title: string
}
This component just renders a heading (h1
) with a string taken from the title
property.
Header
that renders the headingHeaderProps
and use it in the Header
componentRoot
component, pass the title 'User Management'
Location: src/modules/users/user-types.ts
This file contains definitions of user interfaces.
UserName
contains two strings firstName
and lastName
User
is the same as UserName
, but additionally contains an id
(number)Location: src/modules/users/components/user-list.tsx
This component renders a list of the users saved in the state and two buttons to add two different users.
UserList
that renders
Add No One
and Add Mother of Dragons
First Name
and Last Name
. The table displays text No Users
when the list is emptyArya Stark
and the second one Daenerys Targaryen
id
which should be unique within the list (we will not implement deleting users)useState
from ReactuseState
call to add a new userRoot
componentThe main purpose of this exercise is to try stateless components.
01-react-stateful
UserList
component into stateless function componentLocation: src/modules/users/user-types.ts
AddUser
interface, it's a function which takes UserName
as parameter and returns void
Location: src/modules/users/components/user-list.tsx
Props:
{
users: User[],
addUser: AddUser
}
The functionality is the same like in the previous exercise. The only difference is that the logic will be outside the file.
UserList
component into a function that renders users from the users
property (or No Users
when the list is empty)addUser
function taken from the props when the user clicks on the buttonUserProps
interfaceLocation: src/index.tsx
Move logic from the old UserList
into the index file. All application data will be in a global object.
state
with 2 fields (title
and users
)render
that just calls ReactDOM.render
and uses data from the global objectaddUser
that adds the user into the list of users and calls your render
function
state
objectLocation: src/modules/root/components/root.tsx
Props:
{
title: string,
users: User[],
addUser: AddUser
}
Since we moved the logic into the index file and the Root
component receives all necessary props, we need to send props into Header
and UserList
.
Try 3 different versions of the Header
component and see when they get rendered
React.Component
React.PureComponent
The main purpose of this exercise is to try Redux.
02-react-stateless
Location: src/modules/users/user-actions.ts
This file defines actions and action types.
UserActionTypes
of action types with one value addUser = 'users/addUser'
AddUserAction
which extends Action<T>
from 'redux'
for users/addUser
action. In addition to the mandatory type
property this action will contain a payload with the new user details.
{
payload: UserName
}
ActionCreator<A>
type from 'redux'
) called addUser
that takes UserName
object as parameter and returns the AddUserAction
action.UserActions
, which is a union of all user actions. This is useful in the reducer when more actions are defined. Note, currently we have only one action users/addUser
.UserActionCreators
, which congregates all user action creators.Location: src/modules/users/users-reducer.ts
State:
{
title: string,
users: User[]
}
The logic from addUser
function from the previous exercise will be in this reducer.
Reducer<S, A>
from 'redux'
users
field in the state when the users/addUser
action is dispatchedLocation: src/modules/root/root-reducer.ts
This is the main reducer of the whole app. The main purpose is to combine all reducers from all modules into a single reducer.
usersReducer
combineReducers
from redux
to create the root reducer
export type RootState = ReturnType<typeof rootReducer>
Location: src/index.tsx
Configure all necessary things for redux
.
store
with the createStore
function from redux
rootReducer
enhancer
. Use the following to setup the Redux DevTools Extension
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose
}
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
dispatchAddUser
that calls store.dispatch
to dispatch the users/addUser
actionstore.getState()
in your render
functionrender
function with store.subscribe
to re-render the app when an action is dispatchedThe main purpose of this exercise is to try React Redux
and Redux Toolkit
.
03-redux
Provider
from react-redux
instead of the manual re-renderingReact Redux hooks
to get Redux store data in Header
and UserList
Redux Toolkit
Location: src/modules/root/components/header.tsx
The same component like in the previous exercise.
Props:
{
}
useSelector
from React Redux
to get the title
from Redux storeLocation: src/modules/users/components/user-list.tsx
The same component like in the previous exercise.
Props:
{
}
useSelector
from React Redux
to get the users
list from Redux storeuseDispatch
to get the dispatch
function of the Redux store. Use it to dispatch users/addUser
actionLocation: src/modules/root/components/root.tsx
Props:
{
}
Header
and UserList
components don't need them anymore.Location: src/modules/users/users-slice.ts
The same actions and reducer, but created with Redux Toolkit
.
createSlice
from Redux Toolkit
to generate action creators and action types for user reducers.adduser
reducer implementation to this file and pass it with the reducer
option to createSlice
function.actions
and reducer
returned by the createSlice
function as usersActions
and usersReducer
.Location: src/modules/users/user-actions.ts
This file is no longer needed. The user actions are now generated in src/modules/users/users-slice.ts
.
Location: src/modules/users/users-reducer.ts
This file is no longer needed. The addUser
reducer was moved to src/modules/users/users-slice.ts
.
Location: src/index.tsx
Currently, we need only store
and we need to call ReactDOM.render
directly with Provider
component.
render
function and directly call ReactDOM.render
Provider
and put Root
as its childdispatchAddUser
(the action is dispatched in the UserList
component)store.subscribe
(it is not necessary with Provider
)configureStore
function from Redux Toolkit
instead of the createStore
. This will by default enable the Redux DevTools Extension
used in previous exercise. The composeEnhancers
function is no longer needed.The main purpose of this exercise is to try Reselect.
04-react-redux
Header
and UsersList
Location: src/modules/users/users-selectors.ts
getTitle
with createSelector
from reselect
title
string from the state
getUsers
with createSelector
users
array from the state
getUserList
with createSelector
getUsers
selector and modifies last names to upper caseLocation: src/modules/root/components/header.tsx
The same component like in the previous exercise.
getTitle
selector in useSelector
Location: src/modules/users/components/user-list.tsx
The same component like in the previous exercise.
getUserList
selector in useSelector
callAdd a new component, which is a memoized version of <button>
. Use React.memo
.
Props:
React.ButtonHTMLAttributes<HTMLButtonElement>
UserList
component instead of the ordinary buttonsuseCallback
hook from React
to memoize the callbacks passed to memoized buttonsThe main purpose of this exercise is to try Redux-Saga, axios, and Express.
05-reselect
Location: package.json
start
script into the following
"start": "concurrently \"npm run start-fe\" \"npm run start-be\"",
"start-fe": "react-scripts start",
"start-be": "cd backend && nodemon server.ts",
proxy
into the root to correctly handle CORS
"proxy": "http://localhost:3001"
Location: backend/server.ts
A simple express
server that has 2 routes GET /users
and POST /users
.
express
express.json()
and express.urlencoded()
middlewareGET /users
that returns all users from the user list
POST /users
that
id
firstName
and lastName
can be taken from req.body
)id
is included)Location: backend/tsconfig.json
A simple TypeScript configuration file.
tsconfig-base.json
located in the exercises
directorycommonjs
module, which is needed for Node.jsLocation: src/modules/api/api-client.ts
This file contains an API client with axios
that is used to make requests to the BE server.
axios.create
to create the clientbaseURL: 'http://localhost:3000'
in the configLocation: src/modules/users/users-effects.ts
This file defines all effect functions that perform the corresponding requests to API. We need only 2 effects right now - getUsers
and addUser
.
getUsers
that makes a request to GET /users
addUser
that makes a request to POST /users
and sends an object with firstName
and lastName
in the request bodyLocation: src/modules/users/users-slice.ts
We will need a new reducer to store fetched users into the state.
usersLoaded
to do itUsers are added on the BE side, so the addUser
reducer is not needed anymore, but the action type still is.
addUser
reduceraddUser
action creator to the usersActions
export object
createAction
function to create the action creator with type users/addUser
action.type
for stronger typing of sagasLocation: src/modules/users/users-saga.ts
This file is used to create redux sagas that handle side effects to communicate with the BE server. We need 2 sagas to handle all API effects we have - getUsers
and addUser
.
getUsers
that
UsersEffects.getUsers
users/usersLoaded
action with data
taken from the responseaddUser
that
UsersEffects.addUser
getUsers
saga to refresh the user listtry/catch
in both sagasusersSaga
(only this one needs to be exported - with export default
) that
getUsers
saga immediately (we don't have a router currently)addUser
saga when the users/addUser
action is dispatched (hint: use takeEvery
)Location: src/modules/root/root-saga.ts
This file simply starts all sagas that are needed in the whole application. Currently, we have only our own usersSaga
.
rootSaga
that runs usersSaga
(hint: use fork
)Location: src/index.tsx
Configure all necessary things for redux-saga
.
sagaMiddleware
with a function createSagaMiddleware
(default export from redux-saga
)sagaMiddleware
to the configureStore
call with the middleware
propertysagaMiddleware.run(rootSaga)
The main purpose of this exercise is to try normalizr
.
06-redux-saga
Location: backend/server.ts
The server adds skills and set the correct regnal number to every user.
The entity interfaces are
interface Skill {
id: string // e.g. skill-1
name: string
}
interface UserSkill {
skill: Skill
level: number
}
interface User extends UserName {
id: string; // e.g. user-1
regnalNumber: number // use Arabic numerals
skills: Array<UserSkill>
}
string
ids for easier understanding of normalizr
)id
of the user as a string
(e.g. user-1
instead of 1
)level
is somehow based on the regnal number (it doesn't matter what equation you use - it can be for example level = 3 * regnalNumber
)Location: src/modules/entities/entities-schema.ts
This file contains normalizr
schema of our entities.
Skill
, UserSkill
, and User
entitiesid
field, you need to specify one with idAttribute
, which can be a string
(name of the id
field) or a function that creates the value of id
fieldLocation: src/modules/entities/entities-types.ts
This file contains type definitions for normalized user data.
Skill
, UserSkill
and User
types, which are the normalized version of types defined in src/modules/users/user-types.ts
.
id
of an entity instead of nested types:
interface UserSkill {
id: string
skill: string
level: number
}
UserEntities
type, which describes entities
created by the user data normalization:
{
skills: { [key: string]: Skill }
userSkills: { [key: string]: UserSkill }
users: { [key: string]: User }
}
UserIds
type (a string array), which describes the user id
s returned from the normalize
call (result
property).Location: src/modules/users/users-slice.ts
State:
{
title: string,
userIds: string[]
}
userIds
instead of users
UserIds
typeLocation: src/modules/entities/entities-slice.ts
This file contains an entities reducer, which manages the entities repository.
EntitiesState
type/interface, which is equal to UserEntities
. This state would contain all normalized entities used in the application.updateEntities
case reducer which would make a recursive merge of current state with the newly received entities. This is a common approach in the applications where the entities are fetched in small portions.
EntitiesState
objValue
and srcValue
as arguments and return srcValue
if both arguments are arrays. For other types it will return undefined, which indicates no customization.entities
slice with createSlice
function
EntitiesState
for the state type.entitiesUpdated
reducer which updates the entities repository by calling updateEntities
case reducer.entitiesReducer
and entitiesActions
.Location: src/modules/root/root-reducer.ts
The created entities reducer needs to be added into the root reducer.
entitiesReducer
and add it to the root reducerLocation: src/modules/entities/entities-saga.ts
This file contains a saga which normalizes data and stores them to the entities state.
normalizeAndStore
saga:
normalize(data, schema)
to normalize the passed data.normalize
function returns an object with two properties: entities
and result
.entities/entitiesUpdated
action with the payload containing entities
.result
.Location: src/modules/users/users-saga.ts
Currently, the same denormalized data that comes from the BE server are stored in the state. We need to normalize the data from response and store them in the entity repository.
normalizeAndStore
saga to normalize the fetched data and store the entitiesusers/usersLoaded
action to save the user id
s the storeLocation: src/modules/entities/entities-selectors.ts
Since data are stored in the normalized form in the state, we need to denormalize them for easier access to values.
getUsers
, getSkills
, and getUserSkills
) that return the corresponding entities in the denormalized form
mapValues
from lodash
to create a new object with keys identical to source object.Location: src/modules/users/users-selectors.ts
The users reducer doesn't store the entity data, it stores id
s only.
getUserIds
that returns id
s from the redux stategetUsers
selector to map users id
s from the users reducer into denormalized usersgetUserList
selector to return the users with
roman-numerals
library)Location: src/modules/users/components/user-list.ts
regnalNumber
next to the first nameThe main purpose of this exercise is to try router5
and @salsita/react-crud
.
07-normalizr
/users/user-1
)@salsita/react-crud
to automate entity fetchingThe solution of this exercise (08-router5
) uses a separate set of dependencies. You can run npm install
in the corresponding directory to install them.
Location: src/server.js
Add a route to fetch a single user.
GET /users/:id
and return the corresponding user in the responseLocation: src/modules/root/root-reducer.js
We need to add all required reducers into the root reducer.
import { apiReducer as api } from '@salsita/react-api';
import { crudReducer as crud } from '@salsita/react-crud';
import { routerReducer as router } from '@salsita/react-router';
Location: src/router/routes.js
This file contains names and configuration of routes.
const USERS_LIST = 'users'
for the list of all usersconst USER_DETAIL = 'users.detail'
for the detail page (with id
parameter)Location: src/index.js
Use buildRouter
and buildStore
functions for easier configuration of redux
, router5
, and redux-saga
.
import { buildRouter } from '@salsita/react-router';
import { buildStore } from '@salsita/react-core';
buildRouter(routes, options)
function
defaultRoute
in the options
argumentbuildStore(rootReducer, rootSaga, router)
functionLocation: src/modules/users/components/user-detail.js
Props:
{
userDetail: {
firstName: string,
lastName: string,
regnalNumber: string,
skills: Array<{
skill: {
name: string
},
level: number
}>
}
}
This component displays a user detail with skills information.
UserDetail
that displays the dataLocation src/modules/users/components/users-route.js
Props:
{
route: {
name: string
}
}
This component takes care about the proper routing in the users module.
UserDetail
component if the current route ends with detail
(hint: use endsWithSegment
from router5-helpers
)UsersList
componentLocation: src/modules/users/components/users-list.js
We need to add links to the detail page. Navigate to the detail page when the user clicks on the first name.
import { Link } from '@salsita/react-router';
Link
component and set name
and params
props on itLocation: src/modules/root/components/root.js
Use the Route
component from @salsita/react-router
for easier routing. You can also use ApiErrorToast
and ApiLoader
to display a basic error toast and loading spinner. The data for both components are automatically stored in the state from @salsita/react-api
.
import { Route } from '@salsita/react-router';
Route
component instead of UsersList
and set startsWith
and component
props on itimport { ApiErrorToast, ApiLoader } from '@salsita/react-api';
Portal
from react-portal
to render ApiErrorToast
and ApiLoader
Location: src/modules/crud/crud-saga.js
This file contains two important functions for the CRUD module - mapEntityToSaveParams
and mapRouteToFetchParams
. Both of them return params that are used for saving or fetching entities.
mapEntityToSaveParams(entity, isUpdate)
that
entity
is a string
that describes the name of the entity currently being savedisUpdate
is a boolean
flag to distinguish between create and update{
effect: (data: any) => void, // an effect to save the entity
schema: Schema // a schema from normalizr
}
mapRouteToFetchParams(route)
that
{
[identifier]: { // this can be any string that identifies the fetched data
effect: (...effectParams: any) => data, // an effect to fetch the entity
schema: Schema // a schema from normalizr
effectParamsFactory: (state: RootState) => any[] // the result is used for effectParams
}
}
Location: src/modules/crud/crud-entities.js
This file has only string constants with entity names for mapEntityToSaveParams
USER
entityLocation: src/modules/crud/crud-selectors.js
The CRUD module takes care about automatic storing of entity id
s. Since the id
s won't be in the usersReducer
anymore, we need to slightly update UsersSelectors
and move them into CrudSelectors
.
getUsersList
getUserDetail
Location: src/modules/users/users-selectors.js
getUsersList
selector (hint: use CrudSelectors
)Location: src/modules/root/root-saga.js
Start crudSaga
to automatically fetch entities.
import { crudSaga } from '@salsita/react-crud';
crudSaga
(it needs mapRouteToFetchParams
as an argument)Location: src/modules/users/users-effects.js
We need a new effect to fetch a single user. Also, we should use wrapApiCall
from @salsita/react-api
for proper error handling.
import { wrapApiCall } from '@salsita/react-api';
wrapApiCall(effect)
getUser
Location: src/modules/users/users-saga.js
The CRUD module handles entity fetching so we don't need the getUsers
saga anymore. Use the saveEntity
saga from @salsita/react-crud
for better error handling and fetchEntities
to refresh the user list.
saveEntity(data, entity, mapEntityToSaveParams)
saga (instead of the direct effect call) that
data
is an object that is sent to the BE serverentity
is a string
that describes the name of the entity currently being savedmapEntityToSaveParams
is a function that defines defines save params (effect
and schema
) to use.data
field)fetchEntities(route, mapRouteToFetchParams)
saga (to refresh the user list) that
route
is the name of the route you want to refreshmapRouteToFetchParams
is a function that defines fetch params (effect
and schema
) to useLocation: src/modules/users/users-actions.js
We don't need the USERS_LOADED
action anymore.
usersLoaded
action creatorLocation: src/modules/users/users-reducer.js
State:
{
title: string
}
usersLoaded
action handlerThe main purpose of this exercise is to try Redux Form.
08-router5
The solution of this exercise (09-forms
) uses a separate set of dependencies. You can run npm install
in the corresponding directory to install them.
Location: src/server.js
Add routes that updates a user and fetches skills. Modify the route that saves a new user.
PATCH /users/:id
that updates the user and returns the updated user in the responseGET /skills
that returns all skillsPOST /users/:id
and add skills
field into the request bodyLocation: src/modules/root/root-reducer.js
We need to add the form
reducer into the root reducer.
import { formsReducer as form } from '@salsita/react-forms';
form
reducer into the root reducerLocation: src/router/routes.js
const USER_CREATE = 'users.create'
for the form that creates a new userLocation: src/modules/users/users-effects.js
We need new effects to update a user and fetch all skills.
updateUser
getSkills
Location: src/modules/users/users-actions.js
Currently, we have only one action called ADD_USER
that is dispatched when a user clicks on one of the buttons. Since we will use this action to create or update a user, let's rename it to SAVE_USER
.
ADD_USER
action to SAVE_USER
Location: src/modules/crud/crud-saga.js
We will need a list of all skills to display them in the create/edit form. We also have a new effect called updateUser
so we can use it in mapEntityToSaveParams
.
skills
in the USERS_LIST
routeupdateUser
effect into mapEntityToSaveParams
(hint: use the isUpdate
argument to distinguish between create and update)Location: src/modules/crud/crud-selectors.js
getSkills
Location: src/modules/users/components/user-form.js
This form component has fields for firstName
, lastName
, and skills
where a single user can have multiple skills
.
FormField
from @salsita/react-forms
for
firstName
lastName
FormFieldSelect
from @salsita/react-forms
for skills
FieldArray
component from redux-form
firstName
cannot be an empty stringlastName
cannot be an empty stringskills
cannot be empty and must be unique@salsita/react-forms
Location: src/modules/users/components/user-create.js
Props:
{
saveUser: (formData: object) => void
}
This component just renders the UserForm
component to create a new user.
Location: src/modules/users/components/user-detail.js
Props:
{
userDetail: {
firstName: string,
lastName: string,
regnalNumber: string,
skills: Array<{
skill: {
name: string
},
level: number
}>
},
saveUser: (formData: object) => void
}
This component just renders the UserForm
component with initialValues
to edit a user.
Location: src/modules/users/components/users-list.js
Props:
{
users: Array<{
id: number,
firstName: string,
lastName: string,
regnalNumber: string
}>
}
Link
to the USERS
pageLocation src/modules/users/components/users-route.js
We want to display the UserCreate
component in a modal dialog while the users list is shown in the background.
UsersList
and UserCreate
on the create
routePortal
to put the UserCreate
component in the modal dialogLocation: src/modules/users/users-saga.js
There are couple of things we need to update in our sagas.
saveUser
since the new name of the action that starts the saga is SAVE_USER
.
saveUser
saga can be called from two routes now (USERS_LIST
and USER_DETAIL
), we want to redirect the user into USERS_LIST
route after the successful submission.
import { RouterActions } from '@salsita/react-router';
RouterActions.Creators.navigateTo(routeName)
action to perform the redirectsaveEntity
saga, which is the name of the form that was submitted