使用 React 製作 FeedBack UI

前言

此筆記主要在紀錄使用 React 開發時,遇到的知識點,內容看起來可能有點零散,還在學習如何更有系統的產出有使用到框架的筆記…

知識點

  • useState、useEffect 使用方法
  • 初探 json server
  • React router 6

小眉角

vscode 的好用 snippet

rfce 可以產生 React 元件會用到的 function。

1
2
3
4
5
6
7
import React from 'react';

function Test() {
return <div></div>;
}

export default Test;

新增 Header 元件

在 src 資料夾下,新增 components 資料夾,裡面新增 Header.jsx 檔案。

然後新增以下內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import PropTypes from 'prop-types'

function Header({text,bgColor,textColor}) {
const headerStyles = {
backgroundColor: bgColor,
color: textColor,
}
return (
<header style={headerStyles}>
<div className="container">
<h2>{text}</h2>
</div>
</header>
)
}

元件裡 props 的預設參數

可以避免掉當 prop 沒有傳入時,產生錯誤。

在元件上方引入

1
import PropTypes from 'prop-types'

如果 props 沒有值傳進來,就使用這邊的參數。

1
2
3
4
5
6
7
8
9
10
11
12
Header.defaultProps = {
text: 'Feedback UI',
bgColor: 'rgba(0,0,0,0.4)',
textColor: '#ff6a95'
}

// 設定傳進來的參數是什麼型別
Header.propTypes = {
text: PropTypes.string,
bgColor: PropTypes.string,
textColor: PropTypes.string,
}

設定全域的資料

要做的事

  1. 增加狀態(資料)
  2. 新增資料夾
1
2
3
4
5
6
7
*
├─data
└─FeedbackData.js
└─src
├─FeedbackItem.jsx
└─FeedbackList.jsx

使用假資料來了解元件的狀態

以物件陣列的方式設定資料,最後 export ,讓其他檔案可以讀取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const FeedbackData = [
{
id: 1,
rating: 10,
text: 'Lorem ipsum dolor sit amet.',
},
{
id: 2,
rating: 9,
text: 'Lorem ipsum dolor sit amet.',
},
{
id: 3,
rating: 8,
text: 'Lorem ipsum dolor sit amet.',
},
]

export default FeedbackData

取得預設資料

在 App.js 將資料使用 useState 取出資料,FeedbackData 就是資料。

1
const [feedback,setFeedback] = useState(FeedbackData)

再將 資料 使用 props 的方式傳入

1
<FeedbackList feedback={feedback}/>

設定資料到元件裡

這裡會在 App.js 裡,將資料傳入 FeedbackList 元件,然後再傳入 FeedbackItem。

FeedbackList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import FeedbackItem from "./FeedbackItem"

function FeedbackList({feedback}) {
// 如果沒有資料,救回傳 No Feedback Yet,做錯誤處理。
if(!feedback || feedback.length === 0){
return <p>No Feedback Yet</p>
}
return <div className="feedback-list">
// 使用 map 方法,將資料用迴圈的方式渲染出來
{feedback.map((item)=>(
<FeedbackItem key={item.id} item={item} />
))}
</div>
}

export default FeedbackList

FeedbackItem.js

在上一層 (FeedbackList) 以 item 的方式傳入這一層 (FeedbackItem),解構賦值後就可以使用。

1
2
3
4
5
6
7
8
9
10
function FeedbackItem({item}) {
return (
<div className="card">
<div className="num-display">{item.rating}</div>
<div className="text-display">{item.text}</div>
</div>
)
}

export default FeedbackItem

設定 Item 的 Style

將 className 以 style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import PropTypes from 'prop-types'
import Card from './shared/Card'

function FeedbackItem({item}) {
return (
<Card>
<div className="num-display">{item.rating}</div>
<div className="text-display">{item.text}</div>
</Card>
)
}

// 檢查傳進來的型別
FeedbackItem.propTypes ={
item: PropTypes.object.isRequired,
}
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
import PropTypes from 'prop-types'

function Card({children,reverse}) {
// 方法一
return (
<div className={`card ${reverse && "reverse"}`}>
{children}
</div>
)
// 方法二
return(
<div className="card" style={{
backgroundColor: reverse ? 'rgba(0,0,0,0.4)' : "fff",
color: reverse ? '#fff' : '#000'
}}>
{children}
</div>
)
}

Card.defaultProps = {
reverse: false,
}

Card.propTypes = {
children: PropTypes.node.isRequired,
reverse: PropTypes.bool,
}

export default Card

增加刪除功能

要做的事

  1. 新增 React Icon 的 FaTimes Icon
  2. 新增 刪除 Function (filter 方法)

新增點擊後的確認事件 Function

deleteFeedback 傳入 FeedbackItem Component

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const deleteFeedback = (id) =>{
// 當按下叉叉之後,該 item 會被刪除
if(window.confirm('Are you sure you want to delete?')){
setFeedback(feedback.filter((item) => item.id !== id))
}
}

<>
<Header/>
<div className="container">
<!-- deleteFeedback function 以 handleDelete傳入FeedbackList -->
<FeedbackList feedback={feedback} handleDelete={deleteFeedback}/>
</div>
</>

FeedbackList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import PropTypes from 'prop-types'
import FeedbackItem from "./FeedbackItem"

function FeedbackList({feedback,handleDelete}) {
if(!feedback || feedback.length === 0){
return <p>No Feedback Yet</p>
}
return <div className="feedback-list">
{feedback.map((item)=>(
// 將 handleDelete 傳入
<FeedbackItem key={item.id} item={item} handleDelete={handleDelete}/>
))}
</div>
}

FeedbackItem.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { FaTimes } from 'react-icons/fa'
import PropTypes from 'prop-types'
import Card from './shared/Card'

function FeedbackItem({item,handleDelete}) {
return (
<Card>
<div className="num-display">{item.rating}</div>
<!-- 當點擊叉叉之後,就呼叫handleDelete Function(從App.js傳入的) -->
<button onClick={() => handleDelete(item.id)} className="close">
<FaTimes color='purple'/>
</button>
<div className="text-display">{item.text}</div>
</Card>
)

新增 FeedbackStatus 元件

要做的事

  1. 新增 有幾則 Feedback 和 Feedback 的平均分數

計算有幾則 Feedback 與平均分數

  • 使用 reduce 計算平均
  • 使用 正則表達式 處理小數點
  • isNaN 為判斷是否為空,如果為空則回傳 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import PropTypes from 'prop-types'

export default function FeedbackState({feedback}) {
// Calcualte ratings avg
let average = feedback.reduce((acc,cur) => {
return acc + cur.rating
},0) / feedback.length

average = average.toFixed(1).replace(/[.,]0$/, '')
return (
<div className="feedback-stats">
<h4>{feedback.length} Reviews</h4>
<h4>Average Rating: {isNaN(average) ? 0 : average}</h4>
</div>
)
}

FeedbackState.propTypes = {
feedback: PropTypes.array.isRequired,
}

新增 Input 元件

新增元件

  • 使用 onChange 監聽 input 值的變化
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
import { useState,useEffect } from 'react'
import Card from "./shared/Card"

function FeedbackForm() {
const [text,setText] = useState('')

const handleTextChange = (e) =>{
setText(e.target.value)
}

return (
<Card>
<form>
<h2>How would you rate your service with us?</h2>
{/* @todo - rating select */}
<div className="input-group">
<input type="text" placeholder="Write a review"
onChange={handleTextChange} value={text}/>
<button type="submit">Send</button>
</div>
</form>
</Card>
)
}

export default FeedbackForm

調整 Submit 按鈕觸發事件

  • 為 input 選項增加判斷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function FeedbackForm() {
const [text,setText] = useState('')
// 新增兩個 useState
const [btnDisabled,setBtnDisabled] = useState(true)
const [message,setMessage] = useState('')

const handleTextChange = (e) =>{
// 當input的值為空時,button為不能按的狀態
if(text === ''){
setBtnDisabled(true)
setMessage(null)
}else if(text !== '' && text.trim().length <= 8){
// 當字串長度未滿八時,顯示以下的字
setMessage('Text must be at least 10 characters')
setBtnDisabled(true)
}else{
setMessage(null)
setBtnDisabled(false)
}
setText(e.target.value)
}

增加 選擇分數的圈圈

  1. 將點選的數字 Set 到 SetRating 裡
  2. 將每一個選項都使用 checked={selected === 1} 賦予值, onChange 監聽事件
  3. 點選後改變他的數值,在 e.currentTarget.value 前面加上 + 號,就可以使字串轉成數字。
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
import { useState } from "react";

function RatingSelect({ select }) {
const [selected, setSelected] = useState(10);

const handleChange = (e) => {
// console.log(typeof +e.currentTarget.value);
setSelected(+e.currentTarget.value);
select(+e.currentTarget.value);
};

return (
<ul className="rating">
<li>
<input
type="radio"
id="num1"
name="rating"
value="1"
onChange={handleChange}
checked={selected === 1}
/>
<label htmlFor="num1">1</label>
</li>
<li>
<input
type="radio"
id="num2"
name="rating"
value="2"
onChange={handleChange}
checked={selected === 2}
/>
<label htmlFor="num2">2</label>
</li>
...
</ul>
);
}

export default RatingSelect;

增加 Feedback Item

  1. 使用 uuid 讓每一個 item 都擁有唯一的 id
  2. 新增 addFeedback Function
  3. 當 Submit 按鈕按下後,增加一個物件

App.js

1
2
3
4
5
6
import { v4 as uuidv4 } from 'uuid'

const addFeedback = (newFeedback) => {
newFeedback.id = uuidv4()
setFeedback([newFeedback,...feedback]);
}

FeedbackForm.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const handleSubmit = (e) => {
// 如果事件可以被取消,就取消事件(即取消事件的預設行為)。
// 但不會影響事件的傳遞,事件仍會繼續傳遞。
e.preventDefault();
if (text.trim().length > 10) {
// click button 後,送出此物件
const newFeedback = {
text,
rating,
};

handleAdd(newFeedback);
// 清空輸入框
setText('')
}
};

新增 Feedback Item 出現的動畫

  1. 安裝 framer motion
  2. 在產生 Item 的地方,加上 <AnimatePresence>

FeedbackList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
// import framer-motion
import { motion, AnimatePresence } from "framer-motion";

// 加上 AnimatePresence
<AnimatePresence>
{feedback.map((item)=>(
<motion.div key={item.id} initial={{option: 0}}
animate={{opacity:1}} exit={{opacity:0}}>
<FeedbackItem key={item.id} item={item}
handleDelete={handleDelete}/>
</motion.div>
))}
</AnimatePresence>

新增 React Router 6

  1. 新增 React Router 6
  2. 新增 Router、Routes、Route

App.js

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
import { BrowserRouter as Router,Route,Routes} from 'react-router-dom'

return (
// 新增Router
<Router>
<Header/>
<div className="container">
<!-- 增加 Routes -->
<Routes>
<!-- 增加Route、exact、path、element -->
<Route exact path="/" element={
<>
<FeedbackForm handleAdd={addFeedback}/>
<FeedbackState feedback={feedback}/>
<FeedbackList feedback={feedback} handleDelete={deleteFeedback}/>
</>
}>
</Route>
<!-- 增加Route、path、element -->
<Route path='/about' element={<AboutPage/>} />
</Routes>
<AboutIconLink />
</div>
</Router>
)

AboutIconLink.jsx

新增 Link,可以切換路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { FaQuestion } from 'react-icons/fa'
import { Link } from 'react-router-dom'

function AboutIconLink() {
return (
<div className='about-link'>
<Link to='/about'>
<FaQuestion size={30} />
</Link>
</div>
)
}

export default AboutIconLink

AboutPage.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Card from "../components/shared/Card"

function AboutPage() {
return (
<Card>
<div className="about">
<h1>About This Project</h1>
<p>This is a React app to leave</p>
<p>Version: 1.0.0</p>
<p>
<a href="/">Back To Home</a>
</p>
</div>
</Card>
)
}

export default AboutPage

AboutPage.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { FaQuestion } from 'react-icons/fa'
import { Link } from 'react-router-dom'

function AboutIconLink() {
return (
<div className='about-link'>
<!-- React Router 6 中使用 to={{ }} -->
<Link to={{pathname: '/about',}}>
<FaQuestion size={30} />
</Link>
</div>
)
}

export default AboutIconLink

AboutPage.jsx

1
2
3
4
5
6
7
8
9
import { Link } from "react-router-dom"
import Card from "../components/shared/Card"

function AboutPage() {
<p>
<!-- 將 a tag 換成 Link to -->
<Link to="/">Back To Home</Link>
</p>
</Card>

新增 useParams 取得 url

App.js

在 App.js 中,使用 path 的方法,帶入網址參數。

1
<Route path='/post/:id/:name' element={<Post/>} />

Post.jsx

使用 useParams 取得網址參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useParams } from 'react-router-dom'


function Post() {
const params = useParams()

return (
<div>
<h1>Post {params.id}</h1>
<p>Name: {params.name}</p>
</div>
)
}

export default Post;

使用 navigate and nested routes

App.js

使用*代表可以使用多種網址

1
<Route path='/post/*' element={<Post/>} />

Post.js

使用 * 後,就可以使用各種網址。

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
import { Navigate, useNavigate,Routes,Route } from 'react-router-dom'


function Post() {
const status = 200

const navigate = useNavigate()

const onClick = () => {
navigate('/about')
}

if (status === 404) {
return <Navigate to="/notfound"/>
}
return (
<div>
<h1>Post</h1>
<button onClick={onClick}>Click</button>
<Routes>
<Route path='/show' element={<h1>Hello World</h1> }/>
</Routes>
</div>
)
}

新增 conetext 與 provider

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { FeedbackProvider } from './context/FeedbackContext';

// 新增 FeedbackProvider
<FeedbackProvider>
<Router>
<Header/>
<div className="container">
<Routes>
<Route exact path="/" element={
<>
<FeedbackForm handleAdd={addFeedback}/>
<FeedbackState feedback={feedback}/>
<FeedbackList feedback={feedback}
handleDelete={deleteFeedback}/>
</>
}>
</Route>
<Route path='/about' element={<AboutPage/>} />
</Routes>
<AboutIconLink />
</div>
</Router>
</FeedbackProvider>

FeedbackContext.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createContext, useState } from "react";

const FeedbackContext = createContext()

export const FeedbackProvider = ({ children }) => {
const [feedback, setFeedback] = useState({
id: 1,
text: 'This item is from context',
rating: 10,
})
return (
<FeedbackContext.Provider value={{
feedback,
}}>
{children}
</FeedbackContext.Provider>
)
}

export default FeedbackContext

將全域的資料傳到 FeedbackList 元件

FeedbackList.jsx

1
2
3
4
5
6
// 原始寫法
function FeedbackList({feedback,handleDelete}) {

// 更改後的寫法
function FeedbackList({ handleDelete }) {
const { feedback } = useContext(FeedbackContext)

將 Function 移到 Context

將所有會用到的 Function 全部放到 FeedbackContext 中

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
import { createContext, useState } from "react";
import { v4 as uuidv4 } from 'uuid'

const FeedbackContext = createContext()

export const FeedbackProvider = ({ children }) => {
const [feedback, setFeedback] = useState([
{
id: 1,
text: 'This is feedback item 1',
rating: 10,
},
{
id: 2,
text: 'This is feedback item 2',
rating: 1,
},
{
id: 3,
text: 'This is feedback item 3',
rating: 10,
}
])


const addFeedback = (newFeedback) => {
newFeedback.id = uuidv4()
setFeedback([newFeedback,...feedback]);
}

const deleteFeedback = (id) =>{
if(window.confirm('Are you sure you want to delete?')){
setFeedback(feedback.filter((item) => item.id !== id))
}
}

return (
// 放到 value 中
<FeedbackContext.Provider value={{
feedback,
deleteFeedback,
addFeedback,
}}>
{children}
</FeedbackContext.Provider>

增加修改 Item 的 Button

FeedbackList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
const [feedbackEdit, setFeedbackEdit] = useState({
item: {},
edit: false
})

// Set item to be update
const editFeedback = (item) => {
setFeedbackEdit({
item,
edit: true
})
}

增加 useEffect 修改 Item

1
2
3
4
5
6
7
8
9
10
const { addFeedback, feedbackEdit } = useContext(FeedbackContext)

// 修改 item 內容
useEffect(() => {
if (feedbackEdit.edit === true) {
setBtnDisabled(false)
setText(feedbackEdit.item.text)
setRating(feedbackEdit.item.rating)
}
}, [feedbackEdit])

update feedback item

判斷 Edit 是否為 true,如果為 true,就更改 id

1
2
3
4
5
if (feedbackEdit.edit === true) {
updateFeedback(feedbackEdit.item.id, newFeedback)
} else {
addFeedback(newFeedback);
}

add deploy dependent

這裡想要部屬到 Netlify 時,出現了一些無法部屬的問題。查詢了一些解答之後,發現只要加上 mini-css-extract-plugin這個 plugin,就可以解決。

增加 Json Server

利用 json server來建立一個 API Server。

使用 Fetch 從後端取得資料

從利用 Fetch 取得 API 的資料。

FeedbackContext.js

1
2
3
4
5
6
7
8
9
10
11
12
useEffect(() => {
fetchFeedback()
}, [])

// Fetch feedback
const fetchFeedback = async () => {
const response = await fetch(`http://localhost:5000/feedback?_sort=id&_order=desc`)
const data = await response.json()

setFeedback(data)
isLoading(false)
}

增加 Loading component

FeedbackList.jsx

1
2
3
4
5
6
7
8
9
10
11
import FeedbackItem from "./FeedbackItem";
import Spinner from './shared/Spinner';

function FeedbackList() {
const { feedback, isLoading } = useContext(FeedbackContext)
// 判斷是否有資料,如果有的話,顯示資料
if(!isLoading && (!feedback || feedback.length === 0)){
return <p>No Feedback Yet</p>
}

return isLoading ? <Spinner/> : (

新增將資料傳遞到後端的動作

在 package.json 裡,新增 proxy,避免 cros 錯誤。

1
"proxy": "http://localhost:5000",

新增資料到後端

1
2
3
4
5
6
7
8
9
10
11
12
13
// Add feedback
const addFeedback = async (newFeedback) => {
const response = await fetch('/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newFeedback),
})

const data = await response.json()

setFeedback([data,...feedback]);

刪除資料

1
2
3
4
5
6
const deleteFeedback = async (id) =>{
if (window.confirm('Are you sure you want to delete?')) {
await fetch(`/feedback/${id}`, { method: 'DELETE'})
setFeedback(feedback.filter((item) => item.id !== id))
}
}

更新資料

1
2
3
4
5
6
7
8
const updateFeedback = async (id, updItem) => {
const response = await fetch(`/feedback/${id}`, {
method: 'PUT',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(updItem)
})

使用 React 製作 FeedBack UI
https://phoebeho.com/React/20220123/3955701539/
作者
Phoebe
發布於
2022年1月23日
許可協議