六角學院 React 實戰影音學習心得

Lisa Li
48 min readAug 16, 2023

--

為自己投資未來的養分

標示說明:

📚 => 自我理解

📌 => 小知識補充

🌟 => 注意點

2023.08.14

## 元件介紹

  • react-chartjs-2

## 元件基礎練習

  • React 元件一定使用大寫作開頭
  • 定義好元件就可以使用標籤+參數使用元件
  • 如果不需要包括 Chidren 的元件,標籤可以使用單標籤結尾的形式 <Sidebar />

## 建立元件

  • price="1000" 則傳入的為 字串 型別; price={1000} 傳入即為 數字 型別

2023.08.15

## props 解構

  • 可直接在參數解構
const Card = ({title,price})=>{
...
}

## 資料驅動

  • 資料 Array 中 key 一定要補上 id(唯一值)<§1>
  • 📚 和 Vue 直接賦予特定 attr 在 html 不同,React 是使用 js array 的方式 loop 產生畫面

## Key 值很重較

  • 需要使用不會變動的唯一值,不然執行的 function 或是 對應的 input 就不會跟著走─ §1

## 巢狀元件

  • 📌 JSX return 不能直接純數值,需要使用 html tag 或 <>...</> 包起來。`${price}` 修改成這樣寫法就可以了 <>${price}<small>/月</small></>

## 巢狀元件與 Children props

|| 一個自己沒有嘗試過的用法😍||

const PrimaryButton = ({className, children})=>{
return <button type="button" className={`btn ${className}`}>{children}</button>
}
const Card = ({title,callToAction})=>{
...
{
title === '企業版'?
<PrimaruButton className="btn-primary">
<i className="bi bi-gem"></i>{callToAction}<i class="bi bi-gem"></i>
</PrimaryButton>:
<PrimaruButton className="btn-outline-primary">
{callToAction}
</PrimaryButton>
}
}

2023.08.16

## 元件章節 — 套用時間資料

Date.toLoacleDateString('undefined',option)

  • 只有日期
  • 'undefined' 為地區,如果不須特別指定寫 'undefined' 即可。
  • option 為選項,可直接用 {} 做設定
  • Link

Date.toLoacleString('undefined',option)

  • 包含日期 & 時間
  • Link

## 元件章節 — 將程式碼拆分成元件

拆分元件要思考的點

  • 元件可利用性
  • 當特定區塊的邏輯比較複雜時
  • 重複利用的部分拉出來就好

🌟元件在 return 中僅能為一個區塊!如不能再另外再包一層 <div> 可使用 <></> 包起來。

## 關於 React hook

React hook
useState

## 使用 useState 更新資料狀態

const {useState} = React;
const App = () => {
//前者是變數;後者是方法,useState小括弧中的數字為預設值(依照變數希望的型別做設定)
const [num, setNum] = useState(0);
return (
<>
{num}
<button type="button" onClick={()=>{
setNum(num + 1);
console.log(num); //這個數值會與畫面不對,之後會說明 <§2>
}}></button>
</>
)
}

## input 與 useState

  • React 中 input 的 onChange 觸發時機為 每次輸入文字時就會觸發

2023.08.17

## 關於 useState 的原理與運作 1

const { useState } = React;

//#1 原理
function useCustomState(params){
//React 處理
//純值,方法(通知元件,值已更新)
return [params, function dispatchSetState(){}]
}

const App = () => {
//#2 useState 僅能在 Component 內運作
const [num, setNum] = useState(0)

//#3 num 是 ReadOnly,不能直接透過修改(會報錯)
function removeArrData(){
//更新後的值必須和原 params 的型別相同
setArr(arr.fliter((item)=>item !== arr.length))
}
function addArrData(){
setArr([...arr, arr.length + 1])
}
return (<div>
<button type="button" onClick={()=>{num + 1}}>{num}
</button>
</div>
}

## 關於 useState 的原理與運作 2

  • 🌟 週期性函式重新整理,會一直觸發重新註冊,之後會再說明如何使用
  • setState 要放在元件最外層(const App 內)

## 元件化 input 與狀態管理

  • 把外層的狀態 & 寫入方法傳入內層狀態
  • 📌 <label for=''> 在 JSX 要改成 <label htmlFor=''>

## HTML 與 JSX 的標籤屬性

  • class => className
  • checked => defaultChecked
  • (預設值) value => defaultValue
  • for => htmlFor
  • selected => defaultValue = '2'
//HTML
<select>
<option>1</option>
<option selected>2</option>
</select>

//JSX
<select defaultValue="2">
<option value="1">1</option>
<option value="2">2</option>
</select>
  • textarea => defaultValue = '預設內容'
//HTML
<textarea>
預設內容
</textarea>

//JSX
<textarea defaultValue="預設內容" />
  • 加入 html 標籤都要加上結尾 </tag> or <tag />

2023.08.21

  • dangerouslySetInnerHTML 安全性問題
    如果需要加入 html 字串的話需要用以下方式
const htmlTemplate = {
__html: '<div>這是一串文字</div>'
}

<div dangerouslySetInnerHTML={htmlTemplate}></div>

## 取得 Unsplash 遠端資料

API 免費方案一個小時只能打 50 次 request

## 使用 useEffect 取得遠端資料

  • useEffect 為 每次刷新時決定是否要再執行。(📚:和 Vue 的 mounted 有點類似)
  • useEffect 參數 (callback function, [條件]),無條件時只會觸發一次。
const { useState, useEffect } = React;

const App = () => {
useEffect(()=>{
const getPhotos = async () => {
const result = await axios.get(`${api}?client_is=${accessKey}&query=${fliterString}`);
};
getPhotos();
},[]);
}

## useEffect 的生命週期

  • 📌 畫面載入流程
const APP = () => {
console.log(1);

useEffect(()=>{
console.log(3);
},)

return (
<div>
{console.log(2)}
</div>
)
}
  • 觸發條件
const [filterString, setFilterString] = useState['animal']
const [text, setText] = useState['animal']

//每次都會觸發
useEffect(()=>{
console.log(3);
},)

//僅初始化時觸發
useEffect(()=>{
console.log(3);
},[])


//加入 state 作為條件;當 state 更新時則會觸發;可加入多個 state
useEffect(()=>{
console.log(3);
},[filterString, text])

## useEffect 與非同步

  • 不能放在判斷式中,和 useState 一樣,要放在 Component 內的最外層
  • useEffect 的 callback function 只能使用一般函式,如果要立刻執行,可以改寫為立即函式
  • 📌 使用 async 請記得使用 try…catch

## 非同步與元件渲染

  • 📚 和 Vue Compotision API 不同,React 是每個 param 都會有一組 useState。Vue 使用 data 做主要的控管,但 React 比較偏向用 function 控管。可參考:vue 和 react 区别
  • 📌 CSS 樣式:確保 img 的圖片不會變形
/*確保 img 的圖片不會變形*/
.img-cover{
object-fit: cover;
}
  • (📌-來源)在 JSX 中撰寫 inline-style 的話,可以在 HTML 標籤內的 style 屬性中以帶入物件的方式來完成;物件的屬性名稱會是 CSS 的屬性,但有 - 會用「小寫駝峰」來表示;屬性值則是 CSS 的值;使用 , 相隔。
// 定義 inline-style 行內樣式
<div style={{padding: 20px, zIndex:99}}>123</div>

## useState 的狀態更新問題

useState& useEffect& setInterval 相關問題

Que: 為何以下程式碼執行會一直回傳空陣列?

為何以下程式碼執行會回傳空陣列?

Ans: 更新資料狀態時,若非使用按鈕hook 的情況,就有可能出現資料不會更新或非最即時資料的狀況。上方的問題為由於使用 useEffect 建立 setInterval,因此它僅能取得最初一次的 arr 數值 (即 []),故每秒觸發皆會為空陣列。

Result: 將 setArr 修改為 callback function,並在每次執行時取得前一次的狀態,就可以解決這個問題。

//簡化的寫法
setArr((pre)=> [...pre, ...newArray])

## Bootstrap 中的方法介紹

  • Bootstrap 5 使用 JavaScript 展開 Modal 的方法
//先建議實體
const myModal = new bootstrap.Modal('#exampleModal');
console.log(myModal)
//再操作
myModal.show()

2023.08.22

  • Bootstrap 5 使用 JavaScript 新增 Modal 監聽動作
const myModalEl = document.getElementById('exampleModal')
myModalEl.addEventListener('show.bs.modal', event => {
console.log('Modal展開後觸發的動作')
})

## 在 React 中運行 Bootstrap

  • 因為 React 是逐行執行,所以需要使用到 DOM 元素的 function 需要使用 useEffect(等待 JSX 生成之後觸發)
  • 如果需變動唯一 DOM 元素,則需先在外層定義變數名稱,再由 useEffect 做綁定元素,這樣之後在 useEffect 外的 function 才能正常取用。(或是使用 useRef <§3>)

## useRef 取得 DOM(進行 DOM 元素的綁定)

const {useState, useEffect, useRef} = React;
const APP = () =>{
const btnRef = useRef(null); //null 為初始值
//物件傳參考
console.log(btnRef)
}
  • 運行順序:
    1. 前方的程式碼
    2. JSX
    3. useEffect
    => 故如果需要看到最後結果的話,需要把 console.log 放在 useEffect 中
  • 使用 useRef 進行 Bootstrap 5 Modal 綁定
const modalRef = useRef(null);
useEffect(()=>{
const myModal = new bootstrap.Modal(modalRef.current)
myModal.show()
})

## useRef 進階觀念

  • useState 會刷新元件(持續更新狀態);useRef 傳參考的特性所以不會因為生命週期(刷新元件)而影響到它的值(即修改數值也不會觸發更新元件),故可以用 useRef 來呼叫 DOM 元素 ─§3
const APP = () => {
const myModal = useRef(null);
const openModal = () => {
myModal.current.show();
}
useEffect(()=>{
myModal.current = new bootstrap.Modal(modalRef.current)
});
}

## React Hook 章節挑戰:滾動取得新的圖片

  • offsetTop:DOM 元件上方
  • offsetHeight:DOM 元件高度
  • 取得 scroll 高度需要減去瀏覽器的高度
const height = (listRef.current.offsetHeight + listRef.current.offsetTop) - window.innerHeight;
  • 要注意使用 useState 和 useRef 的時機:使用 useState 本身為非同步,會有沒有取得最新狀態(例如:已經透過 hooks 修改過的 state)的情況,這時候就要改成 useRef
  • 📌 useState 使用會讓整個元件刷新,如果不刷新則可考慮使用 useRef

2023.08.23

## React Hook 章節挑戰:useState 與監聽事件的問題

  • 在 useEffect 中放入監聽事件會造成每次觸發時候都會再次建立監聽,故需在結尾處移除監聽

## React Hook 章節挑戰:更新滾動監聽

  • useEffect 移除監聽:在結尾加入 return () => windows.removeEventListener(…) 會再重新觸發 useEffect 條件時執行,可移除掉上次的監聽。
useEffect(()=>{
getPhoto(1, true);

const scrollEvent = () => {

}

//滾動監聽
window.addEventListener(‘scroll’, scrollEvent);
return () => window.removeEventListener(‘scroll’, scrollEvent);
}, [filterString])

## React Hook 章節挑戰:讀取視覺效果

  • 📌 CSS 如下設定就可以直接滿版(不需要加 width 或 height)
.loading{
position:fixed;
left:0;
right:0;
bottom:0;
...
}
  • 📌 CSS 做出毛玻璃效果
.loading{

background-color: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(5px);
}
  • 📌CSS 做出鎖住滾動效果
body{
/*原始*/
overflow: auto;
/*鎖定*/
overflow: hidden;
}
  • 📚 useState 為非同步控制狀態,所以如果需要比較即時的資料還是用 useRef 比較好。|| 這可能需要經驗去做判斷要用哪個🤔||

## React Hook 章節挑戰 — 額外補充項目

Intersection Observer API

  • 檢測用戶滾動是否超出範圍,如果是,即可加載新內容 (Link)
  • 此做法有瀏覽器的版本限制 (Link)

## React Hook 章節挑戰:Bootstrap Modal 取得單張圖片

  • 📚 useEffect 依照條件不同來決定功能要寫在哪個 useEffect 中

2023.08.25

## React 與常見表單元素

  • 多選 Select
const [selectState, setSelectState]=useState([]);

<select
value={selectState}
multiple={true}
onChange={e=>{
setSelectState([[…e.target.selectedOptions].map((option)=> option.value)])
}}
>
  • 建議較複雜的動作可以拉出來寫成控制器會比較好管理程式碼。

## React 與 Checkbox

  • 多選 Checkbox
const [checkList, setCheckList]=useState([]);
const handleCheckList = (e => {
if (e.target.checked) {
setCheckList([…checkList, e.target.checked]);
}else{
setCheckList(checkList.filter((item)=> item !== e.target.value))
}
})

<div>
<label htmlFor=“isCheck”>確認狀態 {check.toString()}</label>
<input id=“isCheck” type=“checkbox”
value={check}
onChange={ e => {setCheck(e.target.checked)}} />
</div>

## React 生命週期補充說明

  • 可以依照參數是否會跟著元件刷新與否來決定要把參數定義在元件外或內。

## 使用 useMemo 進行運算

  • 減少無用的觸發機會增加效能
  • 用法和 useEffect 類似,差別在於 return 的時候,useEffect return 在除第一次之外才執行的動作;useMemo return 要變動的值(📚:和 Vue Computed 一樣都會回傳一個結果的值)
const {useState, useEffect, useMemo}=React

const [price, setPrice]=useState(100);
const [qty, setQty]=useState(1);

const memoData = useMemo(() => {
console.log(“Memo 被觸發了”);
retrun price * qty
},[price,qty]) // price or qty 變動則會觸發

## useMemo 實戰常見運用

  • 搜尋和排序皆為較耗能的動作,因此比較適合使用 useMemo

## memo 元件

  • 外層元件更新的情況下,內層元件也會進行渲染
  • Props 更新時,才重新渲染元件
const {useState, useEffect, useMemo, memo}=React

const DataTable = memo(({filterProducts})=>{
console.log(‘DataTable 已經被渲染了’);
return (<table>
<tbody>
{filterProducts.map((product) => {
return (
<tr key={product.id}>
<td>{product.title}</td>
<td>{product.price}</td>
</tr>
);
})}
</tbody>
</table>)
});

2023.08.31

## 解決函式傳參考問題:useCallback

  • 原因:如果是外層丟入 memo 元件,仍然會因為外層更新導致多次渲染
  • 用途:把函式記憶起來避免過度渲染
  • 當 條件(即 [] 中的參數)改變時會重新更新回傳的函式
  • 🌟需注意數值狀態
  • LINK
const {useState, useEffect, useMemo, memo, useCallback} = React
const handleCheck = useCallback((e)=>{
console.log('handleCheck 已經觸發');
if (e.target.checked) {
setItems([...items, e.target.value]);
} else {
setItems(items.filter((item)=> item !== e.target.value))
}
},[items])

## 另一種生命週期:useLayoutEffect

  • useLayoutEffect can hurt performance. Prefer useEffect when possible.
const {useState, useEffect, useMemo, memo, useCallback, useLayoutEffect} = React
  • 元件初始化的生命週期
    1. function 元件
    2. 建立 virtual DOM
    3. useLayoutEffect
    4. 畫面渲染
    5. 呼叫 useEffect
  • LINK

## 自訂屬於自己的 hook

const useMousePosition = () => {
const [mouse, setMouse] = useState({});

useEffect(() => {
const getMouseEvent = (e) => {
setMouse({
x: e.clientX,
y: e.clientY
})
}
window.addEventListener('mousemove', getMouseEvent)
return () => window.removeEventListener('mousemove', getMouseEvent)
},[])
return mouse;
}

const APP = () => {
const [text, setText] = useState('');
const mouse = useMousePosition()

return(...)
}

## 自訂 hook 除錯方法 useDebugValue

const { useDebugValue } = React

const useMousePosition = () => {
const [mouse, setMouse] = useState({});

useDebugValue(`useMousePosition: ${mouse.x} ${mouse.y}`)

useEffect(() => {
const getMouseEvent = (e) => {
setMouse({
x: e.clientX,
y: e.clientY
})
}
window.addEventListener('mousemove', getMouseEvent)
return () => window.removeEventListener('mousemove', getMouseEvent)
},[])
return mouse;
}

## 使用現成的 hook

2023.09.05

## 使用 useContext 做到跨元件狀態

useContext 跨元件狀態
  • 用於需要跨元件使用狀態的情況
  • 和 Props 的差別在於,userContext 只需要丟入一次就能給元件中任一個層級的 Component 使用
const { useState, useEffect, createContext, useContext } = React

//建立 userContext 的環境
const UserContext = createContext({})
const Login = () => {
//使用解構的方式將值取出來
//useContext(環境名稱)
const { username, setUserName} =useContext(UserContext)
return(<>
<label htmlFor="username">使用者名稱</label>
<input type="text" id="username"
value={username}
onChange= {e => setUserName(e.target.value)}
placeholder="請輸入名稱" /> {username}
<button type="button" onClick={()=>setIsLogin(true)}>登入</button>
</>)
}

const DeepComponent = () => {
const { username } =useContext(UserContext)
return(<div>這是一個深層元件,{username}</div>)
}

const Greeting = () => {
//使用解構的方式將值取出來
//useContext(環境名稱)
const { username, setUserName} =useContext(UserContext)
return(<>
{username}您好,歡迎登入
</>)
}

const APP = () => {
const [username, setUserName] = useState('柴犬');
const [isLogin, setIsLogin] = useState(false);
return (
<>
{//用 UserContext 中的 Provider 將 Component 包起來}
{//透過 value 將要跨元件使用的參數丟入 UserContext 中}
<UserContext.Provider value={{username, setUserName, setIsLogin}}>
{isLogin? <Greeting /> : <LoginForm />}
</UserContext.Provider>
</>
)
}

## useContext 進階

  • 不在 APP 中生成狀態,直接建立在 useContext 中
新增一層 Dashboard 元件
...
const UserProvider = ({children})=>{
const [username, setUsername] = userState('柴犬');
const [isLogin, setIsLogin] = userState(false);
return (
<UserContext.Provider value={{username, setUserName, setIsLogin}}>
{children}
</UserContext.Provider>
)
}
const Dashboard = () => {
const {isLogin} = useContext(UserContext);
return (<>{isLogin ? <Greeting /> : <LoginForm />}</>)
}
const APP = () => {
return (<>
<UserProvider>
<Dashboard>
</UserProvider>
</>)
}

## 使用 useReducer 進行狀態管理

  • 管理多個狀態
const { useState, useReducer } = React;

const App = () => {

//state=狀態, dispatch=方法 (可自定義,此為習慣命名)
const [state, dispatch] = useReducer(
//callback function 必須做 return (如未 return,state 會被清空)
//state=當前狀態, action=行為參數 (可自定義,此為習慣命名)
function(state, action){
console.log(state, action);
//action 來自於 dispatch 的參數
switch (action.type){
case 'INCREMENT':
num: state.num + 1
return //有 return 就不用 break
case 'DECREMENT':
num: state.num -1
return
case 'SET_NUM':
num: action.payload
return
defalut:
return state;
}
}
//,state(物件,可設定預設值)
,{num:0}
)

function increment(){
dispatch({
//type=判斷行為使用(說明要做什麼事情,習慣使用全大寫), payload (可自定義,此為習慣命名)
type:'INCREMENT'
})
}

function decrement(){
dispatch({
type:'DECREMENT'
})
}

function setNumber(e){
dispatch({
type:'SET_NUM',
payload: parseInt(e.target.value)
//input value 預設為字串須轉為數字
})
}

return(
<>
<button type="button" onClick={()=> decrement()}> - </button>
{state.num}
<button type="button" onClick={()=> increment()}> - </button>
<input type="number" value={state.num} onChange={e => setNumber(e)} />
</>
)
}

2023.09.06

## useReducer 實戰範例

  1. 把多個 useState 轉換為 useReducer
  2. 將 function 整合到 useReducer
  3. 微調樣板
  4. 將 useCallback 加回 function 中
  • useReducer 的 callback function 會另外拉出來(習慣命名為 reducer)
  • reducer 的參數不會帶 event,會單純把值帶入,讓 function 只處理資料狀態,不會處理其他事件

## React Create App 是什麼(簡稱 CRA)

## 安裝 CRA

  • 建立 CRA 專案 LINK
npx create-react-app my-app
cd my-app
npm start
  • public(不需要編譯的檔案)
  • src(需要編譯的檔案)
  • - index.js:所有檔案的進入點
  • - reportWebVitals:分析工具

## 使用 CRA 進行開發

  • ./src/assets: 存放CSS & 圖檔
  • ./src/components:存放元件
  • 元件單獨獨立檔案的用法需要在最後加入 export default <元件名稱>
  • 引用的寫法

2023.09.07

## 載入外部套件

  • 使用 npm 安裝外部元件(在專案的終端機下指令)
  • 在CRA的環境下,若已經有 import 部分資源,其他資源直接輸入選取就會自動加入 import 中
  • import 的習慣
  • React.StrictMode 嚴格模式 (LINK):僅會在開發模式下執行,作為檢查錯誤用,可能導致 useEffect 觸發二次。如無影響開發可保留,若影響則可註解

## 啟用 Sass 功能

  1. 安裝 Sass
  2. 新建一個 scss 檔案
  3. 在 App.js 中 import scss 檔案
  4. 可直接在 scss 檔案中定義變數並取用

## 安裝 Bootstrap 樣式庫

  • 可以搭配客製化調整,從 node_modules 複製_variables.scss,並放到專案的 src 中,參考 Bootstrap 的步驟進行 import 即可。

## 編譯完整的版本

  • package.json 中 scripts 為控制編譯的方法
npm run build

#start 可省略 run
npm start
  • build 編譯完成後會將發布的檔案放在新產生 build 的資料夾,需使用 vscode 打開專案,並用伺服器打開

## 環境變數說明(LINK)

  • .env 要放在專案的跟目錄下
  • 在設定與使用
//.env 中
REACT_APP_NOT_SECRET_CODE=123

//component 中
process.env.REACT_APP_NOT_SECRET_CODE

### What other .env files can be used?

Note: this feature is available with react-scripts@1.0.0 and higher.

  • .env: Default.
  • .env.local: Local overrides. This file is loaded for all environments except test.
  • .env.development, .env.test, .env.production: Environment-specific settings.
  • .env.development.local, .env.test.local, .env.production.local: Local overrides of environment-specific settings.

#### Files on the left have more priority than files on the right:

  • npm start: .env.development.local, .env.local, .env.development, .env
  • npm run build: .env.production.local, .env.local, .env.production, .env
  • npm test: .env.test.local, .env.test, .env (note .env.local is missing)

## 快速部署專案至 GitHub Pages

### 推送至 GitHub Repository

  • 本地端在確認修改後先下 git init
  • 使用 GitHub 提供的指令將本地端的資料推送到 GitHub 上的儲存庫

### 部署專案至 GitHub Pages(LINK)

  • homepage 路徑:帳號名稱.github.io/儲存庫名稱
  • 安裝 gh-pages 的套件
  • 新增指令到 scripts
  • npm run deploy 會自動重新編譯再部屬到遠端的儲存庫

2023.09.08

## React Router 基本介紹

## React Router:環境準備

npm install react-router-dom
  • 新增 pages 資料夾:管理頁面元件

## React Router:加入前端路由

  • (專案首次)在 index.js 加入 BrowserRouter
import { BrowserRouter } from 'react-router-dom';

const root = ReactDom.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
  1. 準備頁面元件
  2. 撰寫 Routes
  • 在 App.js 加入 Routes, Route
  • 加入 page 元件
  • 對應 page 元件加入 Route
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';

function App(){
<div className='App'>
<Navbar />
<div className='container mt-3'>
{/*Routes = 輸出內容的區塊*/}
<Routes>
{/*Route = 對應路徑
element = 渲染的元件*/}
<Route path='/' element={<Home/>}></Route>
<Route path='/about' element={<About/>}></Route>
</Routes>
</div>
</div>
}

3. 加入連結

  • 在 Navbar.js 加入 Link
import { Link } from 'react-router-dom';

export default function Navbar() {
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container-fluid">
<a className="navbar-brand" href="#">Navbar</a>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className='nav-item'>
{/*Link = 取代<a>
to = 路由*/}
<Link className='nav-link' to='/'>Home</Link>
</li>
<li className='nav-item'>
<Link className='nav-link' to='/about'>About</Link>
</li>
</ul>
</div>
</div>
</nav>
}

## React Router:巢狀路由與 Outlet

  • 建立巢狀路由元件(Layout<外層> & Index<內層>)
    AlbumLayout.js & AlbunIndex.js
  • 在 AlbumLayout.js 加入 Outlet
import { Outlet } from 'react-router-dom';

export default function AlbumLayout() {
return (
<div className='row'>
<div className='col-4'>左側選單</div>
<div className='col-8'>
<Outlet />
</div>
</div>
)
}
  • 在 App.js 中 AlbumLayout 的路徑下插入子元件 Route
...
{// path:/album }
<Route path='/album' element={<AlbumLayout/>}>
{// 子路由預設的原件,即載入 /album 會觸發的子頁面元件}
<Route index element={<AlbumIndex/>}></Route>
{// path:/album/index }
<Route path='index' element={<AlbumIndex/>}></Route>
</Route>

## React Router:串接 Unsplash API

  • 將值丟入 Outlet 需要使用 context
  • 子元件接收需加入 useOutletContext
//AlbumLayout.js
const [list, setList]=useState([])
...
<Outlet context={list} />

//AlbumIndex.js
import {useOutletContext} from 'react-router-dom'

export default function AlbumIndex(){
const list = useOutletContext();
}

2023.09.11

## React Router:動態路由

  1. 準備單一照片的頁面元件
  2. 撰寫 Routes(動態路由)
import AlbumPhoto from './pages/AlbumPhoto'
...

function App(){
return(
...
{/*:id 為自訂義名稱,影響到的為頁面元件取得的 key 名稱*/}
<Route path=':id' element={<AlbumPhoto />}></Route>
)
}
//AlbumPhoto.js
import { useParams } from 'react-router-dom'

export default function AlbumPhoto(){
//取得 Route 傳進的 id 用
const { id }= useParams()
return {
<>
這是單張圖片
</>
}
}

3. 加入連結

//AlbumLayout.js
import { Outlet, Link } from "react-router-dom";

export default function AlbumLayout(){
...

return(
...
{list.map(itme)=>{
return <li key={item.id}>
<Link to={item.id}>{item.id}</Link>
</li>
}}
...
)
}

2023.09.12

## React Router:搜尋功能製作

  • <a href='#'> 的 href 會影響到路由的運作,所以如果沒有特別用途就改成 span
  • 使用 Enter 觸發搜尋文字,所以使用 defaultValue 來顯示 value
  • Link 可以輸入絕對路徑
<Link to=`/album/{item.id}`>{item.id}</Link>

## React Router:搜尋及網址參數

  • 為了讓頁面在搜尋後返回可以保持原搜尋參數,須將搜尋的 value 放到網址中
  • useSearchParams 操控網址參數
//AlbumSearch.js
const { Link, useSearchParams } from "react-router-dom";
  • 要注意!useEffect 為非同步,會容易有頁面呈現非預期,需多次來回確認結果是否有符合期待

## React Router:導覽連結優化

  • Link 可以連結到特定的頁面但無法縣市啟用到哪個頁面。如有導覽需求,使用 NavLink ,點選時 class 會自動套入 active
{/*isActive:回傳 true 如果有點選*/}
<NavLink className={(isActive)=>{
return `nav-link ${isActive?'activeClassName':''}`;
}} to='/'>

<NavLink style={(isActive)=>{
return {
color: isActive? 'red' : ''
};
}} to='/'>

2023.09.14

## React Router:程式碼微調

  • 將重複的 List 提取出來另外做元件

## React Router:NotFound 頁面與 useNavigate

### NotFound

  • 建立不存在頁面的元件
  • 新增 Route,path 為 *,為所有的路徑。當上方的路徑皆不符合時便會使用該路由。
//App.js
<Route path='*' element={<NotFound />}>

### useNavigate

  • 可使用 useNavigate 來做頁面跳轉
//NotFound.js

import { useNavigate } from 'react-router-dom'

export function NotFound(){
const navigate =useNavigate();

useEffect(()=>{
setTimeout(()=>{
navigate('/');
},2000);
},[navigate])

return(
<>這是不存在的頁面</>
)
}

//App.js
import { useParams, useNavigate } from 'react-router-dom'
...

export default function AlbumPhoto(){
...
const navigate = useNavigate();
...
return (
<>
<button type="button" onClick={()=>{
navigate(-1);
}}>回上一頁</button>
...
</>
)
}

## React Router:React 專案中的 import 手法

  • 整理 App.js 中頁面元件,將元件另外移到一個新的元件(./src/pages/index.js)
//index.js
//方法一
import Home from './Home';
import About from './About';

export{
Home, About
}
//方法二(若頁面元件為預設匯出)
export {default as Home} from './Home'
export {default as About} from './About'
//方法三(若頁面元件為具名匯出)
export { NotFound } from './NotFound'



//App.js
//檔名命名為 index.js 時,路徑'./pages/index'可以縮寫為如下
import { Home, About, NotFound } from './pages'

## React Router:Browser Router 與 Hash Router

  • Browser Router:用於自己控管的伺服器,可以使用 serve 來運行環境(可以在本地端運行)
npm install -g serve
# build 為資料夾名稱
serve -s build
  • Hash Router:用於伺服器非自己能控管的,例:GitHub Pages
import { Hash Router } from 'react-router-dom';

const root = ReactDom.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<HashRouter>
<App />
</HashRouter>
</React.StrictMode>
)

## Redux Toolkit 簡介(LINK)

  • React 提供的工具包

2023.11.20

## Todo 整合 Redux 範例:建立 Redux Toolkit 環境

npm install @reduxjs/toolkit
  • 引入 Redux:建立 src/store.js
//store.js
import {configureStore} from '@reduxjs/toolkit';

export const store = configureStore({
//加入 reducer
})
  • 在 index.js 中 使用 Provider 導入 store
//index.js
import { store } from './store';
import { Provider} from 'react-redux';
...
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
  • 建立 src/slice 資料夾,並在資料夾中建立 todosSlice.js
//todosSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const todosSlice = createSlice({
initialState:[
{
id: 1,
text: '這是一段話'
}
]
})

export default todosSlice.reducer
  • 將 reducer 加入 store.js 中
//store.js
...
import todoReducer from './slice/todoSlice'

export const store = configureStore({
reducer: {
todos: todoReducer,
}
})
  • todosSlice.js 中加入 name 屬性(必要值)
//todosSlice.js
...
export const todosSlice = createSlice({
name:'todos',
initialState:[
{
id: 1,
text: '這是一段話'
}
]
})
  • 在 App.js 取用 Redux
//App.js
import { useSelect } from 'react-redux'
...

const todos = useSelect((state)=>{
console.log(state);
return state.todos //取得 initialState 裡面的 text
})

## Todo 整合 Redux 範例:加入 Actions

建立 Action 匯入元件,才可以用 dispatch 調整狀態

  • todosSlice.js 中加入 reducers,建立新方法,並匯出
//todosSlice.js

export const todosSlice = createSlice({
name:'todos',
initialState:[...],
reducers: { //狀態管理器
createTodo(state, action) {
//state 當前 Slice 的狀態; action 傳入方法的相關參數
console.log(state, action)
console.log(state[0].text, action)
}
}
})

//定義的 reducers 可以使用 actions<=上方的 reducers> 來匯出(具名匯出)
export const { createTodo } = todoSlice.actions;

...
  • 在 App.js 中
  1. 匯入 useDispatch
  2. 匯入剛剛建立的 actions
//App.js
import { useSelect, useDispatch } from 'react-redux'
import { createTodo } from './slice/todosSlice'
...

const dispatch = useDispatch()

function addTodo(){
dispatch(
createTodo({
id: todos.length + 1,
text: newTodoText,
})
);
setNewTodoText(''); //清空 Input
}
//todosSlice.js

export const todosSlice = createSlice({
name:'todos',
initialState:[...],
reducers: { //狀態管理器
createTodo(state, action) {
//state 當前 Slice 的狀態; action 傳入方法的相關參數
console.log(state[0].text, action)
state.push(action.payload)
}
}
})

...

2023.11.27

## Todo 整合 Redux 範例:移除 Todo

  • 在 todosSlice.js 的 reducers 新增移除方法
  • 當 id 相同時移除該項目
//todosSlice.js

export const todosSlice = createSlice({
...
reducers: { //狀態管理器
createTodo(state, action) {...},
removeTodo(state, action) {
const index = state.findIndex((todo) => todo.id === action.payload);
state.splice(index, 1);
}
}
})

export const { createTodo, removeTodo } = todoSlice.actions;
//App.js
import { useSelect, useDispatch } from 'react-redux'
import { createTodo, removeTodo } from './slice/todosSlice'
...

const dispatch = useDispatch()
...

function deleteTodo = (id) => {
dispatch(removeTodo(id));
}

🌟因新增刪除矩陣會導致 key 值重複,因此將 createTodo 中 id 改成時間戳(或任何永遠唯一值)

//App.js
...

function addTodo(){
dispatch(
createTodo({
id: new Date().getTime(),
text: newTodoText,
})
);
setNewTodoText(''); //清空 Input
}

## Todo 整合 Redux 範例:移除 Todo 範例 2

  • 當 id 不同時保留該項目
  • 探討使用覆蓋(🌟非僅改變值)的方始修改 state 會出現的狀況及該如何解決(使用 return 的方式處理)
//todosSlice.js

export const todosSlice = createSlice({
...
reducers: { //狀態管理器
createTodo(state, action) {...},
removeTodo(state, action) {
return state.filter((todo)=> todo.id !== action.payload);
}
}
})

## Todo 整合 Redux 範例:更新 Todo 資料

  • 在 todosSlice.js 新增更新的方法
//todosSlice.js

export const todosSlice = createSlice({
...
reducers: { //狀態管理器
createTodo(state, action) {...},
removeTodo(state, action) {...},
updateTodo(state, action) {
// 1. 取出索引
const index = state.findIndex((todo) => todo.id === action.payload.id);
// 2. 將資料進行寫入
state[index] = action.payload; //🌟僅改變值的話,不需要使用 return
}
}
})

export const { createTodo, removeTodo, updateTodo } = todoSlice.actions;
//App.js
import { createTodo, removeTodo, updateTodo } from './slice/todosSlice'
...

function editTodo = (e) => {
setEditState({
...editState,
text: e.target.value
})
}
function saveEdit = (id) => {
dispatch(
updateTodo({
id,
text: editState.text,
})
);
setEditState(initState);
}
function cancelEdit = () => {
setEditState(initState);
}

--

--