前言 此筆記主要在紀錄使用 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 ;
在 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 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} ) { 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
將 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
要做的事
新增 React Icon 的 FaTimes Icon
新增 刪除 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 ) =>{ if (window .confirm ('Are you sure you want to delete?' )){ setFeedback (feedback.filter ((item ) => item.id !== id)) } } <> <Header /> <div className ="container" > <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 > <button onClick ={() => handleDelete(item.id)} className="close"> <FaTimes color ='purple' /> </button > <div className ="text-display" > {item.text}</div > </Card > )
要做的事
新增 有幾則 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} ) { 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 , }
新增元件
使用 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
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 ('' ) const [btnDisabled,setBtnDisabled] = useState (true ) const [message,setMessage] = useState ('' ) const handleTextChange = (e ) =>{ 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 ) }
將點選的數字 Set 到 SetRating 裡
將每一個選項都使用 checked={selected === 1}
賦予值, onChange
監聽事件
點選後改變他的數值,在 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 ) => { 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 ;
使用 uuid 讓每一個 item 都擁有唯一的 id
新增 addFeedback Function
當 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 ) { const newFeedback = { text, rating, }; handleAdd (newFeedback); setText ('' ) } };
安裝 framer motion
在產生 Item 的地方,加上 <AnimatePresence>
FeedbackList.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 import { motion, AnimatePresence } from "framer-motion" ;<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
新增 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 > <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 > )
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' > <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 > <Link to ="/" > Back To Home</Link > </p > </Card >
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 ;
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 > ) }
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 > <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.jsx
1 2 3 4 5 6 function FeedbackList ({feedback,handleDelete} ) {function FeedbackList ({ handleDelete } ) { const { feedback } = useContext (FeedbackContext )
將所有會用到的 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 ( <FeedbackContext.Provider value ={{ feedback , deleteFeedback , addFeedback , }}> {children} </FeedbackContext.Provider >
FeedbackList.jsx
1 2 3 4 5 6 7 8 9 10 11 12 const [feedbackEdit, setFeedbackEdit] = useState ({ item : {}, edit : false })const editFeedback = (item ) => { setFeedbackEdit ({ item, edit : true }) }
1 2 3 4 5 6 7 8 9 10 const { addFeedback, feedbackEdit } = useContext (FeedbackContext )useEffect (() => { if (feedbackEdit.edit === true ) { setBtnDisabled (false ) setText (feedbackEdit.item .text ) setRating (feedbackEdit.item .rating ) } }, [feedbackEdit])
判斷 Edit 是否為 true,如果為 true,就更改 id
1 2 3 4 5 if (feedbackEdit.edit === true ) { updateFeedback (feedbackEdit.item .id , newFeedback) } else { addFeedback (newFeedback); }
這裡想要部屬到 Netlify 時,出現了一些無法部屬的問題。查詢了一些解答之後,發現只要加上 mini-css-extract-plugin 這個 plugin,就可以解決。
利用 json server 來建立一個 API Server。
從利用 Fetch 取得 API 的資料。
FeedbackContext.js
1 2 3 4 5 6 7 8 9 10 11 12 useEffect (() => { fetchFeedback () }, [])const fetchFeedback = async ( ) => { const response = await fetch (`http://localhost:5000/feedback?_sort=id&_order=desc` ) const data = await response.json () setFeedback (data) isLoading (false ) }
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 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) })