slidenumbers: true
Front-end developer & consultant
- Twitter: @jiayi_ghu
- Github: @jiayihu
- Email: jiayi.ghu@gmail.com
- JavaScript ES6
- React base
- TypeScript
- Redux
- CSS-in-JS
[.background-color: #007ACC]
- Static type-checking
- Correctness by construction
- Auto-documenting code without comments (JSDoc)
- Improved DX
- Inline errors and autocomplete
- Refactoring
- Integration with existing libraries
- Community @types
- Getting the right type can often be difficult
{
"compilerOptions": {
"target": "es5",
"lib": [
"esnext",
"DOM",
"DOM.Iterable"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react"
}
}
--noImplicitAny
--noImplicitThis
--alwaysStrict
--strictBindCallApply
--strictNullChecks
--strictFunctionTypes
--strictPropertyInitialization
function toString(num: number): string {
return String(num);
}
Type assertion
function toString(num: number): string {
return num as string;
}
Inference
function toString(num: number) {
return String(num);
}
boolean, number, string
object
,Array
undefined, null
any
unknown
,never
^ any is like Godfather, forgiving but better not to ask
type Name = string
const inspector: Name = 'Lestrade'
type Id = number
function generateId(): Id {
return Math.random();
}
type Id = number
const generateId: () => Id = () => {
return Math.random();
}
const sample: number[] = [1, 2, 3];
const sample: Array<number> = [1, 2, 3];
const tuple: [number, number, number] = [1, 2, 3];
function useState(initialState: number): [number, (newState: number) => void] {
const setState = (x: number) => {
console.log(x)
}
return [2, setState]
}
const [state, setState] = useState(2)
const user: { id: string, name: string, age: number, email: string } = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
email: 'glestrade@met.police.uk',
}
type User = { id: string, name: string, age: number, email: string }
const user: User = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
email: 'glestrade@met.police.uk',
}
type User = { id: string, name: string, age: number, email: string | null }
const user: User = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
email: null
}
\^ Strict null checks
type User = {
id: string;
name: string;
age: 40;
gender: 'male' | 'female';
email: string | null
};
const user: User = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
gender: 'male',
email: null,
};
enum Gender {
Male = 'male',
Female = 'female'
}
type User = {
id: string;
name: string;
age: 40;
gender: Gender;
email: string | null
};
const user: User = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
gender: Gender.Male,
email: null,
};
enum LogLevel {
OFF,
INFO,
WARNING,
ERROR
}
function setLogLevel(level: LogLevel): void {
}
setLogLevel(LogLevel.ERROR)
setLogLevel(33)
interface User {
id: string,
name: string,
age: number,
email: string | null
}
const user: User = {
id: 'glestrade',
name: 'Inspector Lestrade',
age: 40,
email: null
}
interface User {
id: string,
name: string,
age: number,
email: string | null
}
interface AdminUser extends User {
privilegies: 'admin'
}
interface User { id: string, name: string, age: number, email: string | null }
interface Inspector { id: string, name: string, age: number, email: string | null }
const user: User = { id: 'glestrade', name: 'Inspector Lestrade', age: 40, email: null }
const inspector: Inspector = { id: 'glestrade', name: 'Inspector Lestrade', age: 40, email: null }
function printUserName(user: User) {
console.log(user.name);
}
printUserName(user)
printUserName(inspector)
printUserName({ id: 'glestrade', name: 'Inspector Lestrade', age: 40, email: null })
printUserName({ id: 'mholmes', name: 'Mycroft Holmes', age: 40, gender: 'male', email: null })
interface User {
id: string;
name: string;
age: number;
email: string | null;
\[key: string]: any;
}
function printUserName(user: User) {
console.log(user.someProp);
}
printUserName({ id: 'mholmes', name: 'Mycroft Holmes', age: 40, gender: 'male', email: null });
aka Generics
type Nullable<T> = T | null
type User = { id: string, name: string, age: number, email: Nullable<string> }
type Optional<T> = T | undefined
type User = { id: string, name: string, age: number, email: Optional<string> }
type User = { id: string, name: string, age: number, email?: string }
type Queue<T> = {
head: T,
tail: T[]
}
const queue: Queue<string> = {
head: 'A',
tail: ['B', 'C']
}
interface Queue<T> {
head: T,
tail: T[]
}
const queue: Queue<string> = {
head: 'A',
tail: ['B', 'C']
}
class Stack<T> {
private stack: Array<T> = [];
public push(x: T): void {
this.stack.push(x);
}
public pop(): T {
return this.stack.length ? this.stack.pop() : null;
}
public get length() {
return this.stack.length;
}
}
const queue = new Stack<number>()
interface IQueue<T> {
head: T,
tail: T[]
}
class Queue<T> implements IQueue<T> {
public head: T
public tail: T[]
constructor(head: T) {
this.head = head
this.tail = []
}
}
function id<T>(x: T): T {
return x;
}
const id: <T>(x: T) => T = x => x
const name = id<string>('name')
type ReactElement<P> = {
type: string;
props: P;
key: string | null;
}
type Component<P extends object> =
(props: P) => ReactElement<P> | null;
interface Props {
color: 'string',
onClick: string,
children: React.ReactNode
}
function Button(props: Props) {
return (
<div
className={props.color === 'primary' ? 'btn btn-primary' : 'btn'}
onClick={props.onClick}
>
{props.children}
</div>
)
}
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
function useState<S>(initialState: S | (() => S))
: [S, Dispatch<SetStateAction<S>>];
interface Array<T> {
map<U>(callbackfn: (value: T, index: number) => U): U[];
}
interface Array<T> {
reduce<U>(callbackfn: (accumulator: U, currentValue: T, currentIndex: number) => U, initialValue: U): U;
}
interface Array<T> {
filter<S extends T>(callbackfn: (value: T, index: number) => value is S): S[];
}
function isString(x: unknown): boolean {
return typeof x === 'string'
}
const teacherName: unknown = 'Jiayi'
if (isString(teacherName)) {
console.log(teacherName.toUpperCase());
}
function isString(x: unknown): x is string {
return typeof x === 'string'
}
type SimpleUser = {
id: string;
name: string;
role: 'simple-user';
};
type AdminUser = {
id: string;
name: string;
role: 'admin-user';
privilegies: ['edit', 'delete']
};
type User = SimpleUser | AdminUser
const users: User[] = [
{
id: 'glestrade',
name: 'Inspector Lestrade',
role: 'simple-user'
},
{
id: 'mholmes',
name: 'Mycroft Holmes',
role: 'simple-user'
},
{
id: 'iadler',
name: 'Irene Adler',
role: 'admin-user',
privilegies: ['edit', 'delete']
}
];
const adminUsers = users.filter(user => user.role === 'admin-user')
function isAdmin(user: User): user is AdminUser {
return user.role === 'admin-user'
}
const adminUsers = users.filter(isAdmin)
A type defines the set of values a variable can take.
const age: number = 23
const name: string = 'Jiayi'
const user: { name: string, age: number } =
{ name: 'Jiayi', age: 23 }
type User = { name: string, age: number }
const user: User = { name: 'Jiayi', age: 23 }
const user: User = {
name: 'Jiayi',
age: 23
}
const surnameUser: SurnameUser = {
name: 'Jiayi',
age: 23,
surname: 'Hu'
}
const birthdayUser: BirthDayUser = {
name: 'Jiayi',
age: 23,
birthday: new Date()
}
^ if two types are structurally identical they are seen as compatible.
function isAdultUser(user: User): boolean {
return user.age >= 18
}
isAdultUser(user) // Okay
isAdultUser(surnameUser) // Okay
isAdultUser(birthdayUser) // Okay
const visa: 'visa' = 'visa'
const mastercard: 'mastercard' = 'mastercard'
^ Always have the smallest set possible
type PaymentMethod = 'visa' | 'mastercard'
type User = { name: string, surname: string }
type WithAge = { age: number }
type UserOrWithAge = User | WithAge
type UserWithAge = User & WithAge
[.background-color: #9B59B6]
- Communicazione tra componenti
- Passaggio di dati ed API a più componenti
- Maggior controllo del data-flow: Action > State > View
- Two-way data-binding: State <> View
- Il two-way data-binding è molto comodo, ma può diventare imprevedibile il data-flow
Redux rende i mutamenti allo State prevedibili
- Unico Source of Truth: lo Store
- Ogni intento di modifica dello Store deve essere esplicito: action
- Le funzioni che modificano lo Store sono solo funzioni pure: reducers
- Sono l'unico modo per comunicare una modifica dello State
- Sono semplici oggetti, che descrivono solamente l'intenzione di modificare lo State
- Vengono mandati allo Store via
store.dispatch(action)
const deposit = {
type: 'DEPOSIT', // Obbligatorio
payload: { // Flux Standard Action
amount: 10
}
};
function deposit(amount) {
return {
type: 'DEPOSIT',
payload: { amount: amount }
};
}
- (previousState, action) => newState
- Sono funzioni pure che accettano lo State attuale, l'action e restituiscono un nuovo State
- Il nome deriva dalla funzione reduce degli array
- Modificare i parametri, soprattutto lo State precedente
- Effettuare side-effects come chiamate API al server
- Effettuare azioni non deterministiche
const currentState = {
balance: 20,
operations: [{...}, {...}]
};
const initialState = {
balance: 0,
operations: []
}
function reducer(currentState = initialState, action) {
switch (action.type) {
case 'DEPOSIT':
return Object.assign({}, currentState, {
balance: currentState.balance + action.payload.amount
});
default:
return currentState;
}
}
const newState = {
balance: 30,
operations: [{...}, {...}, {...}]
};
- Contiene lo State dell'applicazione
- Permette la lettura dello State via store.getState()
- Permette la modifica dello State via store.dispatch(action)
- Permette di sottoscriversi agli update via store.subscribe(listener)
import { createStore } from 'redux';
const store = createStore(reducer, initialState);
const state = {
balance: 10000,
operations: {
deposits: [
{
amount: 1000,
description: 'Pagamento fattura n. 002 🍻',
},
],
withdraws: [
{
amount: 500,
description: 'Acquisto nuovo iPhone 7',
},
],
},
user: {
age: 32,
currency: '$',
firstName: 'Giovanni',
lastName: 'Rossi',
sex: 'male',
},
};
- Facilità creare applicazioni universali
- Facilità debuggare o analizzare lo stato dell'applicazione
- Testabile => applicazioni solide
- Facilità persistere lo State come JSON in localStorage o inviato al server
- Miglior error logging/reporting in produzione
- Facilità time-travelling o undo/redo
- NO modifiche per riferimento
obj.a = 2;
array.push(2)
- Ogni modifica segna l'intero percorso come dirty
- User workflow semplici
- No relazioni tra Context
const CurrencyContext = createContext();
export function useCurrency() {
const currency = useContext(CurrencyContext);
if (!currency) {
throw new Error('Cannot be used outside of a CurrencyProvider');
}
return currency;
}
export function CurrencyProvider(props) {
const [currency, setCurrency] = useState(null);
const value = useMemo(() => [currency, setCurrency], [currency]);
return (
<CurrencyContext.Provider value={value} {...props}>
{props.children}
</CurrencyContext.Provider>
);
}
interface FSA {
type: string;
payload?: any;
error?: boolean;
meta?: any;
}
- Action serializzabile
- Se
error: true
allora payload è l'oggetto errore
const usersSelector = (state) => state.users;
function Users() {
const users = useSelector(usersSelector);
return (
<div>
{users.map(user => <span>{user.name}</span>)}
</div>
)
}
- Application state scalabile
- Essenziale per computed derived state
- Application state minimale e UI-indipendent
- Permette memoization (reselect)
function filteredCustomers(state) {
return state.customers.filter((customer) => customer.role === state.filterRole);
}
function customersInvoices(state, customers) {
return customers.map((customer) => {
return {
customer: customer,
invoices: state.invoices[customer.id],
};
});
}
function filteredCustomersInvoices(state) {
const customers = filteredCustomers(state);
return customersInvoices(state, customers);
}
Divide et Impera
const state = {
balance: 10000,
operations: {
deposits: [
{
amount: 1000,
description: 'Pagamento fattura n. 002 🍻',
},
],
withdraws: [
{
amount: 500,
description: 'Acquisto nuovo iPhone 7',
},
],
},
user: {
age: 32,
currency: '$',
firstName: 'Giovanni',
lastName: 'Rossi',
sex: 'male',
},
};
- Divide lo State in porzioni più piccole, affidate a sotto-reducers
- Ogni sotto-reducer gestisce la sua porzione di state in maniera indipendente
- Migliora notevolmente la comprensione del reducer
- Più reducers possono reagire alla stessa action
function operationsReducer(state, action) {
return {
deposits: depositsReducer(state.deposits, action),
withdraws: withdrawsReducer(state.withdraws, action),
};
}
function rootReducer(state, action) {
return {
balance: balanceReducer(state.balance, action),
operations: operationsReducer(state.operations, action),
user: userReducer(state.user, action),
};
}
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
balance: balanceReducer,
operations: operationsReducer,
user: userReducer,
});
export default function balanceReducer(state = 0, action) {
switch (action.type) {
case actionTypes.DEPOSIT:
return state + action.payload.amount;
case actionTypes.WITHDRAW:
return state - action.payload.amount;
default:
return state;
}
}
import { combineReducers } from 'redux';
import * as actionTypes from '../constants/actionTypes';
const initialState = {
deposits: [],
withdraws: [],
};
function depositsReducer(state = initialState.deposits, action) {
switch (action.type) {
case actionTypes.DEPOSIT:
return state.concat(action.payload);
default:
return state;
}
}
function withdrawsReducer(state = initialState.withdraws, action) {
switch (action.type) {
case actionTypes.WITHDRAW:
return state.concat(action.payload);
default:
return state;
}
}
const operationsReducer = combineReducers({
deposits: depositsReducer,
withdraws: withdrawsReducer,
});
- Global namespace
- Dependencies
- Dead code
- Minification
- Sharing constants
- Non-determinism
- Isolation
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn {
}
.btn-primary {
}
.btn {
}
.btn--primary {
}
.btn__text {}
$primary: $blue;
$secondary: $gray-900;
$success: $green;
$info: $cyan;
$warning: $yellow;
$danger: $red;
$light: $gray-200;
$dark: $gray-800;
$body-bg: $gray-200;
$body-color: $gray-600;
$input-btn-padding-y: 0.125rem;
@import 'bootstrap/scss/bootstrap.scss';
.app-nav {
background-color: var(--light);
}
.app-nav .nav-link {
color: var(--dark);
}
.app-nav {
background-color: var(--light);
}
.app-nav .nav-link {
color: var(--dark);
}
.btn {
color: #212529;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
<img className="stock-logo transaction-logo" />
.stock-logo {
align-items: center,
display: flex,
justify-content: center,
height: 50,
overflow: hidden,
text-align: center,
width: 50,
}
.transaction-logo {
background-color: var(--light);
}
img.transaction-logo {
background-color: var(--light);
}
.stock-logo {
align-items: center,
display: flex,
justify-content: center,
height: 50,
overflow: hidden,
text-align: center,
width: 50,
}
.app-nav .nav-link {
color: var(--dark);
}
.my-modal .modal-title {
font-size: 2rem;
}
- Build-time
- CSS Modules
- Treat
- Inline styles
- Radium
- Dynamic classNames
- Styled-components
- Emotion
import { css } from '@emotion/css';
const btnStyle = css({
display: 'inline-block',
fontWeight: 400,
color: '#212529',
textAlign: 'center',
backgroundColor: 'transparent',
border: '1px solid transparent',
padding: '.375rem .75rem',
fontSize: '1rem',
lineHeight: 1.5,
borderRadius: '.25rem',
});
import { css, cx } from '@emotion/css';
import { theme } from './theme';
import { navLinkStyle } from 'bootstrap';
const navStyle = css({
backgroundColor: theme.colors.light;
})
const navLinkStyle = cx(navLinkStyle, css({
color: theme.colors.dark
}))
const btnStyle = css({
color: theme.colors.success;
})
const textStyle = css({
color: theme.colors.success;
})
.css-w7jr6 {
color: var(--success);
}
import { css } from '@emotion/css';
import { theme } from './theme';
const navStyle = css({
backgroundColor: theme.colors.light;
})
<img className={cx(stockLogoStyle, transactionLogoStyle)} />
const stockLogoStyle = css({
alignItems: 'center',
display: flex,
justifyContent: 'center',
height: 50,
overflow: 'hidden',
textAlign: 'center',
width: 50,
})
const transactionLogoStyle = css({
backgroundColor: theme.colors.light;
})
const stockLogoStyle = css({
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
height: 50,
overflow: 'hidden',
textAlign: 'center',
width: 50,
})
const imgStyle = css({
width: '75%'
})
import React from 'react';
import { css, cx } from '@emotion/css';
export type SignedValueProps = {
value: number;
};
export function SignedValue(props: SignedValueProps) {
const { value, className, ...spanProps } = props;
const style = css({
color: value >= 0 ? 'var(--success)' : 'var(--danger)',
});
const cn = cx(style, className);
return (
<span className={cn} {...spanProps}>
{value > 0 ? '+' : '-'}
{props.children}
</span>
);
}