0%

資料整理

例如有兩個 interface 屬性一樣,差異在於 型別內容

1
2
3
4
5
6
7
8
9
10
interface DataA {
key:string
key2:number
}

interface DataB {
key:string
key2:boolean
}

  • 使用 泛型 整理
  • 將 T 部分,設為傳入的變數,可以依據宣告得變數所符合的型別進行設定
    1
    2
    3
    4
    5
    6
    7
    8

    type GenericsObj<T> = {
    key:string
    key2:T
    }
    type DataA = GenericsObj<number>;
    type DataB = GenericsObj<boolean>;
    //以上的 DataA、DataB 的型別會與剛剛 interface 的設定一樣

範例2:

1
2
3
4
5
6
7
8
9
10
interface KeyPair<T, U> {
key: T;
value: U;
}

let kp1: KeyPair<number, string> = { key: 1, value: "str"}
let kp2: KeyPair<string, number> = { key: "str", value: 123}

let arr:number[] = [1,2,3];
let arrTwo:Array<number> = [1,2,3]

payload 應用

  • 這邊以 redux 中整理的 action 為範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type userDataPayload = {
userId: string,
data:any
}
//負責生成 action 的函式
const setUserDataAction = (payload:userDataPayload )=>({
type: 'SET_USER_DATA',
payload
})
const resetUserDataAction =()=>({
type: 'RESET_USER_DATA',
})

type CartDataPayload = {
product: string
}
const setCartDataAction =(payload:CartDataPayload)=>({
type: 'SET_CART_DATA',
payload
})

type SetUserDataAction = {
type: 'SET_USER_DATA',
payload:userDataPayload
}
type ResetUserDataAction = {
type: 'RESET_USER_DATA'
}
type SetCartDataAction = {
type: 'SET_CART_DATA',
payload:CartDataPayload
}

  • 由上可以觀察到 SetUserDataAction、ResetUserDataAction、SetCartDataAction 整理為一個
  • 將 action 的型別整理,其key為type 與 payload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Action<T,P> = {
type: T,
payload:P
}
type ActionWithoutPayload<T> = {
type: T
}

type SetUserDataAction = Action<'SET_USER_DATA',userDataPayload>
type SetCartDataAction = Action<'SET_CART_DATA',CartDataPayload>
type ResetUserDataAction = ActionWithoutPayload<'RESET_USER_DATA'>

//可以將action 中的 type 進行聚合
type Actions = SET_USER_DATA | SET_CART_DATA | RESET_USER_DATA;
  • 以下,進一步再將 Action 和 ActionWithoutPayload 進行整合,他們差異在與 payload
1
type Action<T, P = null> = p extends {} ? {type: T, payload: P} : {type: T};

型別參數動態生成不同型別

函式的泛型

1
2
3
const fn = (param: string | number):(string|number)[]=>[param];
fn(1);//number 陣列 number[];
fn('hello') //string[]

1
2
3
const fn = <T>(param: T):(T)[]=>[param];
fn(1);//number 陣列 number[];
fn('hello') //string[]

  • function 改寫方式
1
2
3
function fn<T>(param:T):T[]{
return [param]
}

type 的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
//共用模板
type GenericList<T> = T[];

type StrList = GenericList<string>;
type BoolList = GenericList<boolean>;

type GenericValObj<T> = {[key:string]:T}
type NumValObj = GenericValObj<number>

const numValObj:NumValObj = {
img_res_200:200
}

  • 嘗試聚合資料
1
2
3
4
5
type GenericUnion<T,U> = T | U | T[] | U[];
type strNumUnion = GenericUnion<string,number>

const val:strNumUnion = "123"
const val2:strNumUnion = ["123"]

將函式改寫為 共用的泛型型別

  • 將type 拉出來獨立撰寫,可以重複使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fn = <T>(param: T):(T)[]=>{
return [param]
};

//上面的函式也可以用於
interface Props {
onFn: typeof fn
}

//rewrite
type GenericFn = <T>(param: T) => T[]
const fn:GenericFn = (param) => {
return [param]
}

//共用,如下有個 interface 其中有個屬性的型別與 GenericFn 一樣,則可以共用
interface Props {
onFn:GenericFn
}

1
2
3
4
5
6
function fn<T>(param:T):T[]{
return [param]
}

//產生型別泛型
type FnType = typeof fn ;


比較<T>位置差異

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type FnTypeA = <T>(param: T) => T[]
type FnTypeB<T> = (param: T) => T[]

const fn1:FnTypeA = (param) => {
return [param]
}

//必須指定好型別的 argument
const fn2:FnTypeB<string> = (param) => {
return [param]
}

//兩個函式在使用上會有差異:
fn1();

fn2('Hi')

  • fn1 可以填入任何內容、可以動態填入型別


  • fn2 只能填入 string 內容
1
2
3
4
5
6
7
export type ClickFn = <Event>(e:Event) => any

//使用 function 會無法重複使用寫好的型別
// 引用
const handleClick:ClickFn = (e)=>{

}

interface 的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export interface GenericI<T>{
[key:string]: T
}
type NumValObj = GenericI<number>;
const numberValObj:NumValObj = {
res:123
}

//type寫法也可以一樣
export type GenericT<T> = {
[key:string]: T
}

//API 的應用!!!
//注意:in 寫法只用於 type 中
export type GenericKeyValObj<T extends keyof any,P> = {
[key in T ]: P
}

//可以應用於 API

type ApiKey = 'user' | 'id' | 'password'; //將欄位值設定好
type UserApiData = GenericKeyValObj<ApiKey, string>

  • 可以很快的生成 API 的資料型別

  • 其他範例

1
2
3
4
5
6
7
8
9
10
11
//類似呼叫API的函式
//Params : 參數
//Res result : 回傳值
type GenericApiFn<Params, Res> = (params:Params)=> Res

//組合其他泛型使用,也可以將原本泛型參數自己使用
interface ApiContainerProps<Params, Res> {
initData: Res;
onAsyncCb: GenericApiFn<Params, Res>
}

  • interface 與 type 的泛型寫法
1
2
3
4
5
6
7
8
//interface 寫函式
interface FnI {
<T>(param: T): T[]
}
//同等於
type FnT = <T>(param:T)=>T[]

const fn:FnT = (param)=>[param]
  • <T>位置的調動
1
2
3
4
5
6
//<T> 會影響整個 interface
interface GenericFnI<T> {
(param: T): T[]
}

const fn2: GenericFnI<string>= (param)=>[param]

過去都未曾注意到泛型可以抽成共用還有 <T> 的位置所放的位置,會影響宣告,透過此次的教學,讓我對泛型可以應用於實際情況的案例,同樣未來在訂定 type 或 interface 時可以加以注意,思考是否可以改寫為泛型加以使用。

參考資料
成為進階TS開發者的第一哩路 — 泛型簡介與基礎(1)
成為進階TS開發者的第一哩路 — 泛型的函式, type和interface寫法一次說清楚!

最近在複習使用 TypeScript 發現還有些部分可以做紀錄並且重新回憶的區塊,藉此再紀錄於部落格。

TS interface的基礎宣告

  • 將複雜系統簡化的結果叫做介面
  • 介面一般首字母大寫
1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
}

let Eva: Person = {
name: 'Eva',
age: 25
};

函式與介面

  • 以下範例宣告 AddFunction 的介面
  • 並將此型別應用於 add 這個變數的方法中
1
2
3
4
5
6
interface AddFunction {
(a: number, b: number): number;
}

let add: AddFunction = function(a, b) { return a + b; };

class與介面

1
2
3
4
5
6
7
8
9
10
interface CatInterface {
//普通成員變數的規格
name: string;
breed: string;
noise: string;

//普通成員方法的規格,使用函式型別格式
makeNoise(): void;
feed(something: string): void
}
  • 若類別想要實踐此介面,必須用 implements 這個關鍵字
  • 如果類別實踐介面的規格中,任何一個成員不見或沒有實踐時,會出現提醒訊息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat implements CatInterface{
public name: string;
public breed: string;
public noise: string = 'Meow meow!';

constructor(name:string, breed:string){
this.name = name;
this.breed = breed;
}

public makeNoise(){
console.log(this.noise);
}

public feed(something: string){
console.log(`${this.name} is eating ${something}...`)
}

}

搭配 interface 常用的方法

  1. extends
  • PersonalInfo 為例
1
2
3
4
5
interface PersonalInfo{
name: string;
age: number;
interesting: string[];
}
  • 假設今天想宣告一個新的使用者帳戶的介面為 UserAccount
    • 除了有一些和客戶相關的規格屬性外,還要包含介面 PersonalInfo 裡的規格
1
2
3
4
5
6
interface UserAccount extends PersonalInfo{
email: string;
password: string;
subscribed: boolean;

}
  • 上面的撰寫方式,等同於
  • 透過使用 extends 延展可以將功用的屬性、型別整理一起
1
2
3
4
5
6
7
8
9
10
11
interface UserAccount{
email: string;
password: string;
subscribed: boolean;

//從PersonalInfo延展而來
name: string;
age: number;
interesting: string[];

}

另外若需要再合併新的型別,也可以透過逗號方式加以延伸

  • 此外還能再多新增 SocialLinks
1
2
3
4
5
6
7
interface SocialLinks {
facebook?: string;
twitter?: string;
linkedin?: string;
website?: ({name:string; url:string})[]

}
  • 同時延展:透過逗號再加入新的
1
2
3
interface UserAccount extends PersonalInfo, SocialLinks{
//...
}

pick

Picking Items with Pick<Type, Keys>

  • 挑選想要的屬性(key)做使用
  • 注意 pick 只能在 type 宣告使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
};

console.log(todo.title); // "Clean room"
console.log(todo.description); // undefined


  • 注意 pick 只能在 type 宣告使用
    1
    2
    interface todo = Pick<Todo, "title" | "completed">;
    // 'Pick' only refers to a type, but is being used as a value here.

omit

Omit<Type, Keys>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};

參考資料
A Detailed Guide on TypeScript Pick Type

一開始看到題目,看到2維陣列就會有些緊張,想著是否要拆開,或是要如何從圖示去轉陣列,寫得很亂又雜還是解不出來。
看完其他教學才知道可以從斜對角開始調換,最後再將左右做調換。

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).

You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var rotate = function (matrix) {
//斜對角
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix.length; j++) {
let temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
//reverse
// Secondly Make Reflected Image Of Matrix
for (let i = 0; i < matrix.length; i++) {
//最左和最右調換,為兩兩調換,所以j的長度為matrix長度除以2
for (let j = 0; j < matrix.length / 2; j++) {
var Temp = matrix[i][j]
//右側最後的值為長度-1
matrix[i][j] = matrix[i][matrix.length - 1 - j]
matrix[i][matrix.length - 1 - j] = Temp
}
}
return matrix;
};

參考資料:
Rotate Image - LeetCode 48 - JavaScript

當發起 dispatch 到 UI 更新之間做什麼事情

  • 希望 dispath 有能力可以再去做其他事情(所以會在過程中去做一些邏輯)

  • 希望過程中可以再做些事情(UI 從 dispatch 到真正渲染之間)


    Middleware

  • 實際上是一個 function

  • next 是 dispatch 的意思,next 所代表的是傳入 action ,會調用下一個 dispatch 來執行所傳入的這個 action

以官網範例:

  • 他是 3 個函式包在一起
1
2
3
4
5
6
const logger = store => nextDispatch => action => {  
console.log('dispatching', action)
let result = nextDispatch(action)
console.log('next state', store.getState())
return result
}

在專案建立
位置:src / middleware

簡易說明

在 return 最後的 function 之前,第二個 function 就是將原有的 dispatch 丟到最後的 function 中,再 return 出去。
在取的 dispatch 之前,會再拿到 store 也是使用 api 時候會回傳進來的東西。
當 middleware 建立好之後,要到 store 當案進行宣告

combineReducers

  • 建立的 slice夾中可能會有多個 reducer,例如 todoReducer, friendReducer, phoneReduce…

Middleware 與 store 的建立

在 middleware 去寫功能的時後,主要分為兩個部分

  1. 在 dispach 之前要做什麼事情
  2. 在 dispach 之後要做什麼事情

RTK Query

使用 middileware 去定義我們的 reducers 會分成多個階段

  • pedding
  • success
  • error
    針對每一次 API 取資料,加入到 reducers 中,此方式會使得 reducer 變得龐大、複雜。
    RTK Query 協助將所有關於 call API 或是 獲取資料過程的功能,將他包裝成一個攻能,並且完全獨立於 reducer 或 redux 的邏輯
  • 可以使用 RTK Query 中的 hook 去獲取當前 data , error 狀態等
    官方文件

建立 api

educerPath => 最後會產生 reducer ,所包含內容會有 pedding, success,error 等狀態
baseQuery => 就是放入 baseUrl
endpoint => 放入 query 資訊
any : 表示會回傳一個 any 結果
string : 需要傳入 string 的 input

json placeholder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const todoApiService = createApi({

reducerPath: 'todoApi',

baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),

endpoints: (builder) => ({

// getPokemonByName: builder.query<Pokemon, string>({

// query: (name) => `pokemon/${name}`,

// }),

getTodoApi: builder.query<any, string>({

query: (id) => `todos/${id}`,

})

}),

})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
// 以下會自動對應產生
export const { useGetTodoApiQuery } = todoApiService

至 store 新增 todoAPI

引用 API hook

import { useGetTodoApiQuery } from './services/todoApi';

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import './App.css';
import styled from 'styled-components'
import { useAppSelector, useAppDispatch } from './hooks';
import { addTodo, addTimestamp } from './slice/todo'
import React, { useState } from 'react';
import { useGetTodoApiQuery } from './services/todoApi';

const Wrapper = styled.div`
padding: 1.5rem;
`

const Title = styled.h2`
font-weight: 900;
margin-top: 2rem;
`

const NoteInput = styled.input`
width: 100%;
height: 40px;
border-radius: 10px;
padding-left: .5rem;
box-sizing: border-box;
`

const SubmitBtn = styled.button`
width: 100%;
box-sizing: border-box;
height: 40px;
background: #000;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
border-radius: 10px;
border: 0;
font-weight: 900;
margin-top: 1rem;

:active {
background: #000000be;
}
`

const Item = styled.div`
margin-top: 1rem;

> b {
margin-right: .5rem;
}
`


function App() {
const todoReducer = useAppSelector(state => state.todoReducer)
const todoList = todoReducer.todoList
const dispatch = useAppDispatch()
const [text, setText] = useState("")
const { data, error, isLoading } = useGetTodoApiQuery('1')
//api 獲取資料
console.log('data:', data)
console.log('error:', error)
console.log('isLoading:', isLoading)

//處裡 data 可能 undefined 的問題
const { userId = 'N/A', title = 'N/A' } = data || {}

return (
<Wrapper>
<Title>TODO LIST</Title>
<NoteInput type="text" value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value)
}} />
<SubmitBtn onClick={() => {
if (text === '') {
alert('請輸入TODO內容')
return
}
dispatch(addTodo(text))
setText('')
}}>
Submit
</SubmitBtn>
<SubmitBtn onClick={() => {
dispatch(addTimestamp())
}}>
Record Timestamp
</SubmitBtn>
<Title>List</Title>
{
todoList.map((todo, index) => {
return <Item key={todo}>
<b>{index + 1}</b>
{todo}
</Item>
})
}
<Title>List2</Title>
{
isLoading && <p> 正在載入資料...</p>
}
{
!isLoading &&
(
<div>
{/* <p>User ID:{data?.userId}</p>
<p>Usr Title:{data?.title}</p> */}
<p>User ID:{userId}</p>
<p>Usr Title:{title}</p>

</div>
)


}
</Wrapper>
);
}

export default App;

若是使用 react thunk

  • 這裡的 middleware 的 reducer 中的 status 要自己去定義
  • 優點:可以完全掌控自己 reducer 的內容
  • 但相對也會較為繁瑣

安裝:
官網安裝

此範例來自 Bruce 前端課程,使用 todoList 來練習

  • 從官網學習基礎設定
  1. 建立 store 同時,撰寫 reducer(會自動產生出對應 reducer 的 action)
    1. action 如同 dispatch 所送出的包裹
    2. reducer 會去拆解包裹,以了解用戶想做的事
      應用於 todo-list
  2. store 中會存有幾條備忘錄,幾條 todo視像要做
  3. 建立資料夾 src / slice ,此 slice 中包含 reducer, action
    TS-設立tool-kit
    建立 store 的方式,要先建立 reducer

src / slice / todo.ts

  • 建立 slice,並注意要傳入三個參數
    • name : slice 的名字
    • initialState 初始化的 state,建立初始狀態得值以及型別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { createSlice } from '@reduxjs/toolkit'
// Define a type for the slice state
interface todoState {
todoList: Array<string>
}
const initialState: todoState = {
todoList: []
}
export const todoSlice = createSlice({
name:'todo',
initialState,
reducers:{
//state:當下狀態
//action:對應的動作
addTodo: (state,action)=>{
state.todoList.push(action.payload)
},
addTimestamp:(state)=>{
state.todoList.push(Date.now().toString())
}

}
}
})

在此範例中會有兩個 dispatch

  1. submit
  2. record timestamp
  • dispatch 會發出上面兩個 action
  • reducer 要兩個事件接收
    最後的 export todoSlice
  • todoSlice 中會包含 actions , reducer , 以及其他 API

建立 store

  1. 位置 src / store.ts
    1
    import { configureStore } from '@reduxjs/toolkit'

從上面過程,可以看到我們 store, action , reducer 都撰寫完成

使用

  • 回到 UI 觸發狀態改變的地方
    1. index.tsx 裝入 store
      • 像是 context 的概念作為 context provider 的內容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Provider } from "react-redux";
import store from './store'
ReactDOM.render(

<React.StrictMode>

<Provider store={store}>

<App />

</Provider>

</React.StrictMode>,

document.getElementById('root')

);

拿到 store 中的 todoList 來使用渲染於畫面

  • 要如何拿到 store ? 使用 useSelector
  • 在這裡要注意因為使用 TS 所以會有類型定義問題!
    • src / hooks.tsx
  1. 型別首先要來自於 store 檔案
  • ReturnType: 幫我們將 store 中的內容,直接導出對應的 type

export type RootState = ReturnType<typeof store.getState>

  1. 回到 hooks 建立定義好型別的 API
1
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
  1. 可以 到 App 檔案或相關的子組件都可以調用 store 的內容
    • 可以拿到預設的 state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useAppSelector } from './hooks';
function App() {

const todoReducer = useAppSelector(state => state.todoReducer)

const todoList = todoReducer.todoList


return (

<Wrapper>

<Title>TODO LIST</Title>

<NoteInput type="text" />

<SubmitBtn>

Submit

</SubmitBtn>

<SubmitBtn>

Record Timestamp

</SubmitBtn>

<Title>List</Title>

{

todoList.map((todo, index) => {

return <Item key={todo}>

<b>{index + 1}</b>

{todo}

</Item>

})

}

</Wrapper>

);

}



export default App;

狀態更新

  • 使用 dispatch 一些 action
  • 定義 useDispatch
  1. hooks
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'

    import type { RootState, AppDispatch } from './store'


    //自定義 hook

    // Use throughout your app instead of plain `useDispatch` and `useSelector`

    export const useAppDispatch: () => AppDispatch = useDispatch

    export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
  2. store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { configureStore } from '@reduxjs/toolkit'

import todoReducer from './slice/todo'

const store = configureStore({

reducer: {

todoReducer

}

})


// Infer the `RootState` and `AppDispatch` types from the store itself

//匯出對應的類型
export type RootState = ReturnType<typeof store.getState>

export type AppDispatch = typeof store.dispatch

export default store

到 App.tsx 使用 dispatch

  • 宣告 dispatch
  • 設置 setState 的值
  • 綁定於 input 並讓 input onchange 時去觸發值得改變
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import './App.css';

import styled from 'styled-components'

import { useAppSelector, useAppDispatch } from './hooks';

import { addTodo, addTimestamp } from './slice/todo'

import React, { useState } from 'react';


function App() {

const todoReducer = useAppSelector(state => state.todoReducer)

const todoList = todoReducer.todoList

const dispatch = useAppDispatch()

const [text, setText] = useState("")

return (

<Wrapper>

<Title>TODO LIST</Title>

<NoteInput type="text" value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {

setText(e.target.value)

}} />

<SubmitBtn onClick={() => {

if (text === '') {

alert('請輸入TODO內容')

return

}

dispatch(addTodo(text))

setText('')

}}>

Submit

</SubmitBtn>

<SubmitBtn onClick={() => {

dispatch(addTimestamp())

}}>

Record Timestamp

</SubmitBtn>

<Title>List</Title>

{

todoList.map((todo, index) => {

return <Item key={todo}>

<b>{index + 1}</b>

{todo}

</Item>

})

}

</Wrapper>

);

}



export default App;

總結步驟

  1. 定義 slice 中的內容,其中包含 初始的資料定義、 reducers
    1. reducers 會透過 toolkit 這工具,將對應得 action 產生出來
    2. action 可以在 dispatch 做發送時帶出去
  2. 定義 provider 作為將 store 資料傳入的橋樑
  3. store 的產生:來自於將 slice 中的 reducers 建立好之後可以隨之建立的內容
  4. 建立 hooks :撰寫自定義的 hook ,將原本內建的 hook 進行重新包裝

前段時間練習了一些 React 的基礎,對於 JSX 以及一些 hook 的用法比較有概念,所以就以 Bruce 的 React 課程實作再來練習,樣式部分以 Tailwind 為主此外也搭配 TS 來撰寫。

1. Header

  • 製作 IGHeader 元件,因為到不同頁面都會有 header 存在,所以可以將它放在外面的共用元件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const IGHeader: React.FC = () => {
const go = useNavigate()
return (
<header className="sticky top-0 border-b-[1px] bg-white border-gray-300 ">
<div className="flex justify-between items-center h-[60px] px-2 lg:max-w-[1024px] lg:mx-auto lg:px-0">
<img
className="w-[100px] cursor-pointer"
src="/images/logo.svg"
/>
<div className="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-4 cursor-pointer"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
onClick={() => go('/')}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-4 cursor-pointer"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
onClick={() => go('/following')}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
<div className=" h-6 w-6 text-white bg-gray-900 font-bold flex rounded-full justify-center items-center">
E
</div>
</div>
</div>
</header>
)
}

2. IGContainer

頁面佈局設定,除了 限時動態頁面會呈現外,在追蹤者頁面也會使用到

  • 使用 Tailwind 設定左右版面以及當螢幕不同尺寸的設定
  • 將樣式寫在(style.div 裏面),作為一個組件 export 出去

3. 限時動態列表(IGStory)

  • 因隸屬於 home 裡面的位置,所以可以將檔案,放在 home 資料夾中,屬於 home 中的組件
    //通常在一份專案中不用引入兩個 css 框架
    實作:
    1. List Container 的容器
      文件
    • 在組建內部要使用 JS 相關的邏輯的寫法,要在要面用大括號刮起來
    • 每一個小圖都是一個 item ,所以可以先製作組件

限時動態 item 組件

  • 此組件建立其中的小項目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'

type ItemProps = {
name: string;
avatar: string;
}

const Item: React.FC<ItemProps> = ({ name, avatar }) => {
return (
<div className=" text-center ">
<div className=" rounded-full w-[56px] h-[56px] ring-2 border-2 border-white ring-red-500 mx-[11px] p-[3px] "
style={{
backgroundImage: `url(${avatar})`,
backgroundPosition: "center",
backgroundSize: "cover",
}}>
</div>
<p className="text-xs mt-1">{name}</p>
</div>
)
}

export default Item

外層

  • 在外層的主要檔案,就是將模板建立好,讓 item 可以 map 進去
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const IGStory: React.FC = () => {
const storyData = [
{
"id": 1,
"name": "bruce_fe",
"avatar": "/images/avatars/a1.png"
},
{
"id": 2,
"name": "max",
"avatar": "/images/avatars/a2.png"
},
{
"id": 3,
"name": "fm",
"avatar": "/images/avatars/a3.png"
},
{
"id": 4,
"name": "joanne",
"avatar": "/images/avatars/a4.png"
},
{
"id": 5,
"name": "focus",
"avatar": "/images/avatars/a5.png"
},
{
"id": 6,
"name": "louis",
"avatar": "/images/avatars/a6.png"
},
{
"id": 7,
"name": "alvin",
"avatar": "/images/avatars/a7.png"
},
{
"id": 8,
"name": "grace",
"avatar": "/images/avatars/a8.png"
},
{
"id": 9,
"name": "rance",
"avatar": "/images/avatars/a9.png"
},
{
"id": 10,
"name": "bruce_fe",
"avatar": "/images/avatars/a10.png"
}
]
return (
<div className='w-full flex items-center h-[110px] box-border overflow-x-auto overflow-y-hidden no-scrollbar shadow-md lg:my-8'>
{/* <Item name="Mike" avatar='/images/avatars/a1.png' /> */}
{
storyData?.map((item) => {
const { id, name, avatar } = item
return <Item key={id} name={name} avatar={avatar} />
})
}
</div>
)
}

export default IGStory

3. User 組件

  • 使用者資訊表,在不同區塊都有重複出現,所以可以抽成共用組件
  • 現定義好 props 的型別
  • 依照藥的版型來切出樣貌,將變數帶入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type IGUserProps = {
size?: "medium" | "small";
showFollow?: boolean;
isFollowing?: boolean;
account?: string;
location?: string;
avatar?: string;
id?: number;
}

const IGUser: React.FC<IGUserProps> = ({
size = "small",
showFollow = false,
isFollowing = false,
account,
location,
avatar,
id,
}) => {
return (

<div className="flex h-[70px] items-center box-border px-4 ">
<div className={`${size === "small" ? "w-[40px] h-[40px]" : "w-[60px] h-[60px]"} overflow-hidden rounded-full `}
style={{
backgroundImage: `url(${avatar})`,
backgroundPosition: "center",
backgroundSize: "cover",
}}
></div>
<div className="ml-4">
<p className="text-sm font-bold">{account}</p>
<p className="text-gray-400">{location}</p>
</div>
{showFollow && (
<p
className={`${isFollowing ? "text-gray-700" : "text-blue-400"} text-xs ml-auto cursor-pointer font-bold`}
> {isFollowing ? "FOLLOWING" : "FOLLOW"}</p>
)

}
</div>
)
}
  • memo 對於重複渲染,效能優化使用

border-box

post 組件

  • 分為三個部分
  1. user
  • 可以使用前面所製作的 user 組件
  1. img
  • 直接放入該項圖片
  1. 評論區
  • 也可以製作成小組件,最後再放入 post 組件
  • 注意傳入 props 要先設定好傳入的型別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React from 'react'

type CommentsProps = {
likes: number;
description: string;
hashTags: string;
createTime: string;
account: string;
}

const Comments: React.FC<CommentsProps> = (
{
likes,
description,
hashTags,
createTime,
account,
}) => {
return (
<div className='px-4'>
<div className='flex items-center justify-between box-border my-4'>
<div className='flex py-1'>
// svg
</div>
<p className='text-sm font-bold'>{likes} likes</p>
<p className='text-sm font-bold'>{description} </p>
<p className='text-blue-400 font-bold'>{hashTags} </p>
<p className='text-gray-400 font-[500] text-xs mt-2'>View all 999 comments </p>
<p className="text-gray-400 text-[10px] mt-1">{createTime}</p>
</div>
)
}



profile 區塊

  • 實作快速,直接做出簡易的版面

  • 套用前面所做的 IGUser 即可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import IGUser from "components/IGUser"

const IGProfile = () => {
const followingList = [
{
id: 1,
location: "Singapore",
account: "max_999",
isFollowing: true,
avatar: "/images/avatars/a1.png",
},
{
id: 2,
location: "Singapore",
account: "fm_999",
isFollowing: true,
avatar: "/images/avatars/a2.png",
},
{
id: 3,
location: "Singapore",
account: "joanne_999",
isFollowing: true,
avatar: "/images/avatars/a3.png",
},
{
id: 4,
location: "Singapore",
account: "focus_999",
isFollowing: true,
avatar: "/images/avatars/a4.png",
},]

return (
<div className="mt-8 ml-6 border-box shadow-md px-4">
<div>
<IGUser size='medium' account='Tmommy_0814' avatar="/images/avatar.png" />
</div>
<p className="text-gray-400 px-4 mt-2">You are Following</p>
<div >
{
followingList?.map((item) => {
const { location, account, isFollowing, avatar, id } = item
return <IGUser location={location} account={account} isFollowing={isFollowing} avatar={avatar} key={id} showFollow />
})
}

</div>
</div>
)
}

export default IGProfile

小結

此次練習在使用 Tailwind 有些不習慣,主要是在於要稍微記得他的關鍵字並去文件查找屬性來應用,待使用到第三的區塊切版時就變得快速些~
另外,使用在課程中 Bruce 也會去說明每個區塊的切割以及重用性,可以將檔案分別放在哪個檔案,以利未來維護。
過去可能拿版面並未想太多,或是沒有多餘時間分析每一頁面的某些區塊是否有重複,我們可以依據建立開關或傳入的資料區紹維變動下版面,就可以達到一起使用 component 的效果。

Given an array nums of size n, return the majority element.

The majority element is the element that appears more than ⌊n / 2⌋ times. You may assume that the majority element always exists in the array.

Example 1:

1
2
Input: nums = [3,2,3]
Output: 3

Example 2:

1
2
Input: nums = [2,2,1,1,1,2,2]
Output: 2

在一開始觀察題目,會直覺想說要找出陣列中出現最多次的元素,此外也注意到題目說明有提及運用 n/2 與陣列長度比較出現次數。
在一開始會傾向使用 雜湊表 來進行解題,如同找出陣列為一值的方式找出答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var majorityElement = function (nums) {
let newObj = {}
for (let num of nums) {
// console.log(num)
(!newObj[num]) ? newObj[num] = 1 : newObj[num]++
}
// console.log(newObj)
let nNum = nums.length / 2
// console.log('nNum', nNum)
for (let key in newObj) {
if (newObj[key] > nNum) return key
}

};

另外也發現可以使用,Boyer–Moore majority vote algorithm(摩爾投票算法)來解題。
宣告變數 result , 以及計算變數。
將陣列loop ,判斷陣列元素有無出現進行 count 的加減
majorityElement2([4, 5, 5, 4, 4]) 為範例:
第一個元素 4 ,因為 count === 0 , result === 4。
進入第二個元素 5,因為 result === 4 進入判斷式 else => count– 。 所以 count === 0 。
進入第三個元素 5,因為 result === 5 進入判斷式 result === elem => count++。 所以 count === 1 。
進入第四個元素 4,因為 result === 5 進入判斷式 else => count– 。 所以 count === 0 。
進入第五個元素 4,因為 result === 4 進入判斷式 result === elem => count++。 所以 count === 1 。

在迴圈的最後元素 4 ,為目前 count 為 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

var majorityElement2 = function (nums) {
let result;
let count = 0;

for (let elem of nums) {
if (count === 0) {
result = elem;
}
if (result === elem) {
count++;
} else {
count--;
}
}

return result;

};


  1. Majority Element II
    此題也是使用摩爾投票法進行解題

Given an integer array of size n, find all elements that appear more than ⌊ n/3 ⌋ times.

Example 1:

1
2
Input: nums = [3,2,3]
Output: [3]

Example 2:

1
2
Input: nums = [1]
Output: [1]

Example 3:

1
2
Input: nums = [1,2]
Output: [1,2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var majorityElement = function (nums) {
// 只要是 > n/3 就進入陣列
let candidate1 = null;
let candidate2 = null;
let count1 = 0;
let count2 = 0;

for (let num of nums) {

if (candidate1 === num) {
count1++
} else if (candidate2 === num) {
count2++
} else if (count1 === 0) {
candidate1 = num;
count1++
} else if (count2 === 0) {
candidate2 = num;
count2++
} else {
count1--;
count2--;
}
}

console.log('data:', candidate1, candidate2)
count1 = 0;
count2 = 0;
const res = [];

limit = Math.floor(nums.length / 3)

for (let num of nums) {
if (candidate1 === num) count1++;
else if (candidate2 === num) {
count2++;
}
}
if (count1 > limit) res.push(candidate1)
if (count2 > limit) res.push(candidate2)
console.log(res, count1, count2)
return res

};
majorityElement([1, 2])

Given an array, rotate the array to the right by k steps, where k is non-negative.

Example 1:

1
2
3
4
5
6
Input: nums = [1,2,3,4,5,6,7], k = 3
Output: [5,6,7,1,2,3,4]
Explanation:
rotate 1 steps to the right: [7,1,2,3,4,5,6]
rotate 2 steps to the right: [6,7,1,2,3,4,5]
rotate 3 steps to the right: [5,6,7,1,2,3,4]
1
2
3
4
5
6
7
Example 2:

Input: nums = [-1,-100,3,99], k = 2
Output: [3,99,-1,-100]
Explanation:
rotate 1 steps to the right: [99,-1,-100,3]
rotate 2 steps to the right: [3,99,-1,-100]

1.由上述範例可以觀察到,根據 k 值進行抓取最後的元素至最前面,以此類推下去。
當 k 為某值的時後,會由陣列最後方取的該數目元素。
以下為直覺解法:

1
2
3
4
5
6
7
8
9
10
11
//會有 問題:Time Limit Exceeded
const rotateArray1 = function (nums, k) {
//unshift 將給入的元素值放到最前面
for (let i = 0; i < k; i++) {
console.log('nums.pop()', nums.pop())
nums.unshift(nums.pop());
}
console.log('num', nums)
return nums;
}

2.也可以先將陣列進行反轉 reverse ,再依據 k 值 來進行前後段區分,來進行反轉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const revNum = (nums, start, end) => {
while (start < end) {

[nums[start], nums[end]] = [nums[end], nums[start]]
console.log('[nums[start], nums[end]]', [nums[start], nums[end]])
start++;
end--;
}
console.log(nums)
}
// revNum([1, 2, 3, 4], 0, 3)

const rotateArray2 = function (nums, k) {
k = k % nums.length
//先整個陣列 reverse
nums.reverse()
revNum(nums, 0, k - 1)
revNum(nums, k, nums.length - 1)
}

想找專案練習來更認識測試的運用,至 youtube 找尋測試相關的教學。

建立 login 檔案

  • login.js : 製作登入元件

  • login.test.js : 撰寫測試

確認 輸入框 有渲染至文件

  • 使用 getByPlaceholderText 取得輸入框元素
1
2
3
4
5
6
7
8
9
10
11
12
<form>
<input
type="text"
placeholder="username"
value=''
/>
<input
type="password"
placeholder="password"
value=''
/>
</form>
1
2
3
4
5
6
7
8
9
10
11
test("username input should be rendered", () => {
render(<Login />);
const usernameInputEl = screen.getByPlaceholderText(/username/i);
expect(usernameInputEl).toBeInTheDocument();
});

test("password input should be rendered", () => {
render(<Login />);
const passwordInputEl = screen.getByPlaceholderText(/password/i);
expect(passwordInputEl).toBeInTheDocument();
});

確認畫面上有按鈕

  • getByRole 取得按鈕
1
2
3
4
5
test("button should be rendered", () => {
render(<Login />);
const buttonEl = screen.getByRole("button");
expect(buttonEl).toBeInTheDocument();
});

輸入框一開始為空

1
2
3
4
5
6
7
8
9
10
11
test("username input should be empty", () => {
render(<Login />);
const usernameInputEl = screen.getByPlaceholderText(/username/i);
expect(usernameInputEl.value).toBe("");
});

test("password input should be empty", () => {
render(<Login />);
const passwordInputEl = screen.getByPlaceholderText(/password/i);
expect(passwordInputEl.value).toBe("");
});

輸入框為空時,按鈕應為 disabled

  • 只要取得按鈕元素並使用 toBeDisabled()
1
2
3
4
5
test("button should be disabled", () => {
render(<Login />);
const buttonEl = screen.getByRole("button");
expect(buttonEl).toBeDisabled();
});

錯誤訊息的提示

  • 確保一開始進入畫面,該訊息不應該出現
  • 可以看見 toBeVisible() ,反之期望無法看見,就需加入not
1
2
3
4
5
test("error message should not be visible", () => {
render(<Login />);
const errorEl = screen.getByTestId("error");
expect(errorEl).not.toBeVisible();
});

輸入框的輸入事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test("username input should change", () => {
render(<Login />);
const usernameInputEl = screen.getByPlaceholderText(/username/i);
//模擬傳入的值
const testValue = "test";

fireEvent.change(usernameInputEl, { target: { value: testValue } });
expect(usernameInputEl.value).toBe(testValue);
});

test("password input should change", () => {
render(<Login />);
const passwordInputEl = screen.getByPlaceholderText(/password/i);
const testValue = "test";

fireEvent.change(passwordInputEl, { target: { value: testValue } });
expect(passwordInputEl.value).toBe(testValue);
});

當輸入框有值的時候,按鈕不應該 disabled

  • 期望按鈕可以點擊
  • 在 jsx 檔案內要設置在輸入框沒有值的時候才是 disabled

1
2
3
4
5
6
7
8
9
10
11
12
13
14

test("button should not be disabled when inputs exist", () => {
render(<Login />);
const buttonEl = screen.getByRole("button");
const usernameInputEl = screen.getByPlaceholderText(/username/i);
const passwordInputEl = screen.getByPlaceholderText(/password/i);

const testValue = "test";

fireEvent.change(usernameInputEl, { target: { value: testValue } });
fireEvent.change(passwordInputEl, { target: { value: testValue } });

expect(buttonEl).not.toBeDisabled();
});

css 補充

1
style={{ visibility: error ? "visible" : "hidden" }}

類似: display:none和visibility:hidden

而兩者差異:
visibility:的隱藏,是物件的位置仍舊保持著不會消失,
display: none, 會將 html 該物件拔除

小結

此練習範例,很請楚地知道在建立一個功能元件之後,如何再加入每一個區塊的測試,如標題文字、輸入框、按鈕的測試等等,也可以觀察到測試根據使用情境一步步的拆解並加入測試
下一篇會有串接資料搭配測試,敬請期待

參考資料
github-login
React Testing Tutorial with React Testing Library and Jest

使用 JavaSript 要進行測試最常使用的就是使用 Jest ,另外依據使用的前端框架來搭配使用進行測試。
以下內文就先以一些範例來直接學習

testing-library

  • 當使創建一個 react 檔案,會預設加入的測試範例檔

    1
    2
    3
    4
     test('renders learn react link', () => {
    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
    });
  • screen : it represent the whole document which we have rendered

  • getByText: 取得 text

    • getByText(/learn react/i)
    • expect(linkElement).toBeInTheDocument();
    • 期待變數得值,toBeInTheDocument()
    • 檢測此項目是否存在於文件之中

撰寫測試的參考文件

ByRole
role attribute

加入列表(list)測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import logo from './logo.svg';

import './App.css';

import Login from './components/Login/Login';



function App() {



return (

<div className="App">

<header className="App-header">

<img src={logo} className="App-logo" alt="logo" />

<p>

Edit <code>src/App.js</code> and save to reload.

</p>

<a

className="App-link"

href="https://reactjs.org"

target="_blank"

rel="noopener noreferrer"
. >

Learn React

</a>

<ul>

<li>Apple</li>

<li>Banana</li>

<li>Kiwi</li>

</ul>


</header>

</div>

);

}

export default App;
1
2
3
4
5
test('renders 3 list items', () => {
render(<App />);
const listItems = screen.getAllByRole("listitem");
expect(listItems.length).toEqual(3);
});
  • jest expect
  • 需要時到官網看相關使用的API
    expect(listItems.length).toBe(3);
    expect(listItems.length).toEqual(3);

測試 title

  • getByTestId:運用此方法可以直接取得在元素上設置的 data-testid
  • getByTitle : 取得在標籤設置 title 的元素
1
2
3
4
5
<!-- const a = 2
const b = 1 -->

<h1 data-testid="mytestid">Hello</h1>
<span title="sum">{a + b}</span>
1
2
3
4
5
6
7
8
9
10
11
12
13
test('renders title', () => {

render(<App />);
const title = screen.getByTestId("mytestid");
expect(title).toBeInTheDocument();

});

test('sum should be 3', () => {
render(<App />);
const titleSum = screen.getByTitle("sum");
expect(titleSum.textContent).toBe("3");
});

運行測試

npm run test

  • 透過測試所提供的錯誤提示,可以得知預期的結果以及實際運行後得到的值之間的差異

參考資料:
[Day29] React Testing Library 的一些實用的小技巧
Next.js | 初探單元測試,使用 Jest + React Testing Library