前言 先不用實際去後端寫 API Endpoint,直接用 Dummy data  來熟悉整套 Workflow,以列出 Dummy data 產品列表為例,這樣可以更快熟悉整套開發流程,並且可以先把前端的畫面先做出來,等到後端準備好後再直接接上 API Endpoint
後端 setup 
建立 Django 專案 
建立、啟用虛擬環境 
下載並輸出此專案的套件清單(corsheaders、JWT、drf…) 
 
1 2 3 4 5 django-admin startproject backend pip install ... pip freeze > requirements.txt 
前端 setup 
建立 React + Vite + TypeScript 專案 
下載所有所需額外套件(@tanstack/react-query、axios…) 
 
1 2 3 4 5 npm create vite@latest my-app --template react-ts cd  my-appnpm install 
整合 tailwindCSS v4 及 shadcn/ui 導入 tailwindCSS v4 官方教學 ,現在步驟變更少,但跟 shadcn/ui 整合的方式跟以往不同了:
跑完 tailwindCSS v4 流程並確定沒問題後,到 shadcn/ui 官方教學  這一步一步做
到 tsconfig.json 加入 compilerOptions
1 2 3 4 5 6 7 8 9 10 11 {  "files" :  [ ] ,   "references" :  [ {  "path" :  "./tsconfig.app.json"  } ,  {  "path" :  "./tsconfig.node.json"  } ] ,   "compilerOptions" :  {    "baseUrl" :  "." ,    "paths" :  {     "@/*" :  [ "./src/*" ]    }   }  } 
然後執行
再到 vite.config.ts 加入相關 resolve 設定
1 2 3 4 5 6 7 8 9 10 11 12 ... import  path from  "path" export  default  defineConfig ({ plugins : [react (), tailwindcss ()],  resolve : {   alias : {   "@" : path.resolve (__dirname, "./src" ),   },  }, }); 
注意這邊若直接複製官方教學的 pnpm dlx shadcn@canary init 會發現報不認得 dlx 指定的錯誤,所以這邊改成執行
就能看到成功訊息並可以開始選擇 base color 了
1 2 3 4 5 6 7 8 9 10 ✔ Preflight checks. ✔ Verifying framework. Found Vite. ✔ Validating Tailwind CSS config. Found v4. ✔ Validating import alias . ? Which color would you like to use as the base color? › - Use arrow-keys. Return to submit. ❯   Neutral     Gray     Zinc     Stone     Slate 
接下來直接測試看是否能直接調用元件來使用,看看 Navbar 能否正常顯示,執行:
1 npx shadcn@latest add navigation-menu 
此時就能看到 components 資料夾底下多了 ui/navigation-menu.tsx 檔案,
設定 React Query Client 設定 React Query Provider,這樣在任何組件內就都能使用 useQuery
1 2 3 4 5 6 7 8 9 10 11 12 import  { QueryClient , QueryClientProvider  } from  "@tanstack/react-query" ;const  queryClient = new  QueryClient ();ReactDOM .createRoot (document .getElementById ("root" )!).render ( <React.StrictMode >    <QueryClientProvider  client ={queryClient} >     <App  />    </QueryClientProvider >   </React.StrictMode >  ); 
統一管理 API 請求,建立 API instance 
設定 baseURL,避免每次請求都寫完整 API 網址 
設定超時時間 (timeout) 
統一 headers 
處理 interceptors 來攔截請求 & 回應(例如自動加上 token) 
 
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 import  axios from  "axios" ;const  api = axios.create ({ baseURL : "https://dummyjson.com" ,   timeout : 5000 ,   headers : {   "Content-Type" : "application/json" ,  }, }); api.interceptors .request .use (  (config ) =>  {      const  token = localStorage .getItem ("token" );   if  (token) {    config.headers .Authorization  = `Bearer ${token} ` ;   }   return  config;  },  (error ) =>  {   return  Promise .reject (error);  } ); api.interceptors .response .use (  (response ) =>  response,  (error ) =>  {   console .error ("API 發生錯誤:" , error);   return  Promise .reject (error);  } ); export  default  api;
有了 baseURL,往後就能像 getProducts() 這樣使用 api 來請求 https://dummyjson.com/products ,不用每次都手動寫 baseURL!
1 2 3 4 5 6 7 8 9 import  api from  "./axiosInstance" ;import  type { ProductListResponse  } from  "../types/products" ;export  const  getProducts = async  (): Promise <ProductListResponse > => { const  { data } = await  api.get ("/products" );   return  data; }; `` ;
定義 types/ 來管理 TypeScript 型別 這樣 TypeScript 會確保 API 回傳的數據符合預期類型,不符合也能在編譯時期立即發現錯誤,在上方 getProducts() 中,就能確保回傳的數據是 ProductListResponse 類型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export  interface  Product  { id : number ;  title : string ;  description : string ;  price : number ;  discountPercentage : number ;  rating : number ;  stock : number ;  brand : string ;  category : string ;  thumbnail : string ;  images : string []; } export  interface  ProductListResponse  { products : Product []; } 
建立 hooks 來處理 API 請求 此專案使用 @tanstack/react-query 來處理 API 請求,與傳統這種 reducers 的方式不同:
需要手動 dispatch 各種狀態 (FETCH_START, FETCH_SUCCESS, FETCH_ERROR) 
沒有 cache 機制,每次載入頁面都要重新請求 
沒有背景同步,數據可能會過時 
 
傳統 reducer 寫法 1 2 3 4 5 6 7 8 9 10 11 12 13 export  const  productListReducer  = (state = { products: [] }, action ) => { switch  (action.type ) {   case  PRODUCT_LIST_REQUEST :     return  { loading : true , products : [] };   case  PRODUCT_LIST_SUCCESS :     return  { loading : false , products : action.payload .products , page : action.payload .page , pages : action.payload .pages  };    case  PRODUCT_LIST_FAIL :     return  { loading : false , error : action.payload  };   default :    return  state;  } }; 
使用 useQuery useQuery 會自動處理 loading、error、data 等狀態,並且有 cache 功能,避免重複請求:
3 行程式碼就完成 useReducer 的所有功能 
內建 isLoading, data, error 
內建快 cache,5 分鐘內不重複請求 
 
1 2 3 4 5 6 7 8 9 10 11 import  { useQuery } from  "@tanstack/react-query" ;import  { getProducts } from  "../api/products" ;export  const  useProducts  = ( return  useQuery ({   queryKey : ["products" ],   queryFn : getProducts,   staleTime : 1000  * 60  * 5 ,   }); }; 
在 React 頁面元件中使用 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 import  { useProducts } from  "../hooks/useProducts" ;import  { Product  } from  "../types/products" ;const  HomePage  = ( const  { data, isLoading, isError, error } = useProducts ();  if  (isLoading) return  <p > 載入中...</p >   if  (isError) return  <p > 發生錯誤:{error?.message}</p >   return  (   <div  className ='p-5' >     <h1  className ='text-2xl font-bold mb-4' > 產品列表</h1 >     <div  className ='grid grid-cols-3 gap-4' >      {data?.products.map((product: Product) => (      <div  key ={product.id}  className ='border p-4 rounded shadow' >        <img  src ={product.thumbnail}  alt ={product.title}  className ='w-full h-40 object-cover rounded'  />        <h2  className ='text-lg font-semibold mt-2' > {product.title}</h2 >        <p  className ='text-sm text-gray-600' > {product.description}</p >        <p  className ='text-lg font-bold mt-2' > ${product.price}</p >       </div >      ))}    </div >    </div >   ); }; export  default  HomePage ;
成功畫面 
        
  
    
      若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
    
  
 
        
        
        
        
        
  
        
        
         
      
      
    
      
    
      
      
    
留言版