iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
自我挑戰組

打造一個糖尿病自我監測小工具:從0開始學前端系列 第 25

Day25打造一個糖尿病自我監測小工具:從0開始學前端

  • 分享至 

  • xImage
  •  

經過思考後,我決定配合專題的技術使用 react ,所以打算把正個網頁都改成使用react 語言去撰寫。邊做邊學。

Step 1:建立 React 專案

npm create vite@latest my-app
cd my-app
npm install
npm run dev

(相片)

Step 2:將功能切成元件

在資料夾找到 my-app / src / App.jsx,打開後從裡面修改後儲存點擊:http://localhost:5173/,就可以即時看到頁面上的變化。再來要開始搬移頁面

report.html

這個頁面原本的功能有:

  • localStorage 讀取 records
  • 產表格
  • 用 Chart.js 畫「餐前/餐後血糖」折線圖
  • 正確清除 Chart 實例,避免重複建立
  1. 先安裝會用到的套件

    npm i chart.js
    
  2. 將路徑變成:src / pages / Report.jsx ( pages 是資料夾,Report.jsx 為檔案 )

  3. 將 report.html 轉換成 Report.jsx,因為這是第一個轉換的頁面,所以會把程式碼貼上來,邊解析邊學習,之後轉換的頁面會直接放上成果。

    引入 React Hooks & Chart.js

    import { useEffect, useMemo, useRef, useState } from "react";
    
    • useState → 我想要「記住一個值,改了會刷新畫面」。
    • useEffect → 我想要「畫面出現或值改了,就順便做某些事」。
    • useRef → 我想要「存東西在抽屜裡,不會因刷新丟失」。
    • useMemo → 我想要「計算過的結果,不要白白重算」。

    註冊 Chart.js 元件

    import {
      Chart,
      LineController,
      LineElement,
      PointElement,
      LinearScale,
      CategoryScale,
      Title,
      Tooltip,
      Legend,
    } from "chart.js";
    

    讓 Chart.js 知道「要用折線圖」並啟用需要的元件。

    小工具函式:血糖狀態判斷

    function getGlucoseStatus(v) {
      const val = parseInt(v, 10);
      if (isNaN(val)) return "未知";
      if (val < 70) return "過低";
      if (val <= 140) return "正常";
      if (val <= 200) return "偏高";
      return "過高";
    }
    

    輸入一個血糖數字 → 回傳「過低、正常、偏高、過高」,這邊寫法雖然跟 HTML 一樣,但:

    1. 寫法看起來一樣,但作用域不同
      • HTML:掛全域
      • React:封裝在元件裡,不會污染其他地方
    2. 跟 UI 綁定的方式不同
      • HTML:要自己操作 DOM,像 tbody.appendChild(row)
      • React:只要在 JSX 用 {getGlucoseStatus(...)},React 來更新畫面
    3. 可重用性
      • 在 React 裡,可以把這個函式抽到 utils.js,任何元件 import 就能用,不會互相干擾。

    紀錄狀態管理

    const [records, setRecords] = useState([]);
    
    useEffect(() => {
      const raw = localStorage.getItem("records");
      try {
        setRecords(raw ? JSON.parse(raw) : []);
      } catch {
        setRecords([]);
      }
    }, []);
    
    • const [records, setRecords] = useState([]);
      • 這行是準備一塊「會記住資料的小白板」:名字叫 records
      • 一開始白板是空的 [](因為還沒讀資料)。
      • setRecords 就是「拿筆把白板改掉」的工具。
      • 等你 setRecords(新資料),React 會自動重畫畫面,表格就會出現資料。
    • useEffect(() => { ... }, []);
      • useEffect =「畫面出來後要去做的事」。
      • 後面的 [] 代表:只做一次(元件第一次出現在畫面後)。
      • 這裡的「要做的事」就是去 localStorage 把舊資料拿回來
    • const raw = localStorage.getItem("records");
      • localStorage 只能存「字串」。
      • getItem("records") 會拿到你以前 setItem("records", "....") 存的那串字。
      • 如果之前沒存過,會拿到 null
    • setRecords(raw ? JSON.parse(raw) : []);
      • 如果 raw 有值,就用 JSON.parse(raw) 把字串變回陣列/物件
      • 如果 raw 沒值(是 null),那就用 [](表示沒有任何紀錄)。
      • setRecords(...) 一做,畫面會重渲染,你的表格會從「空」變成「有資料」。
    • catch { setRecords([]); }
      • 如果有一天 localStorage 裡被塞進去的不是合法 JSON(例如被手動改壞了),

        JSON.parse 會報錯。

      • 我們用 try/catch 把它擋住,保底就直接用 [],避免整個頁面掛掉。

    • }, []);(依賴陣列是空的)
      • 這表示這個 Effect 只在第一次載入時執行
      • 時間軸會是這樣:
        1. 元件第一次畫 → records 先是 [] → 畫面可能顯示「目前沒有紀錄」
        2. Effect 立刻跑去 localStorage 讀資料 → 呼叫 setRecords(真的資料)
        3. React 重新畫畫面 → 表格出現舊紀錄

    紀錄轉成圖表資料

    const { labels, bfData, afData } = useMemo(() => {
      const labels = records.map((r) => r.date);
      const bfData = records.map((r) => Number(r.bf_glucose) || null);
      const afData = records.map((r) => Number(r.af_glucose) || null);
      return { labels, bfData, afData };
    }, [records]);
    
    • labels:橫軸的日期
    • bfData:餐前血糖陣列
    • afData:餐後血糖陣列
    • useMemo,只有當 records 改變時才重新計算,提升效能。

    畫 Chart.js 圖表

    const canvasRef = useRef(null);
    const chartRef = useRef(null);
    
    useEffect(() => {
      if (!canvasRef.current) return;
    
      // 如果之前有圖 → 銷毀,避免重疊
      if (chartRef.current) {
        chartRef.current.destroy();
        chartRef.current = null;
      }
    
      if (labels.length === 0) return;
    
      const ctx = canvasRef.current.getContext("2d");
      chartRef.current = new Chart(ctx, {
        type: "line",
        data: {
          labels,
          datasets: [
            { label: "餐前血糖", data: bfData, borderColor: "blue", ... },
            { label: "餐後血糖", data: afData, borderColor: "red", ... },
          ],
        },
        options: { ...圖表設定... },
      });
    
      return () => {
        if (chartRef.current) {
          chartRef.current.destroy();
          chartRef.current = null;
        }
      };
    }, [labels, bfData, afData]);
    
    • canvasRef:對應 <canvas> 元素,拿到 2D 繪圖 context
    • chartRef:存 Chart 實例,方便在 re-render 或卸載時銷毀
    • labelsbfDataafData 有變 → 重新畫圖
    • 最後 return 的函式是清理動作(避免重複畫好幾個圖層)

畫面結構

<nav>...</nav>
<h1>糖尿病病患紀錄表</h1>

<table>... 表格顯示每筆紀錄 ...</table>

<div>
  <canvas ref={canvasRef} />
</div>
  • nav:簡單的超連結導覽(之後建議換成 Link
  • table:用 records.map 把每一筆紀錄畫成 <tr>
  • canvas:用來放 Chart.js 畫出來的折線圖
  1. 把路由接上

    App.jsx 使用 react-router-dom,把 /report 指到這個元件:

    ( 剛剛寫的 Report.jsx 就是元件 )

    // src/App.jsx
    import { Routes, Route } from "react-router-dom";
    import Home from "./pages/Home";
    import Report from "./pages/Report";
    
    export default function App() {
      return (
        <>
          {/* 可以放自己的 NavBar 元件 */}
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/report" element={<Report />} />
          </Routes>
        </>
      );
    }
    

    不過在這之前還有其他事要做。

    • 先安裝 React Router
    npm install react-router-dom
    
    • 建立不同頁面 ( 先把其他頁面建立起來 ),先建個首頁

    Home.jsx

    export default function Home() {
      return (
        <div style={{ padding: "16px" }}>
          <h1>糖尿病管理系統</h1>
          <p>這是我的第一個 React 頁面 🚀</p>
          <a href="/report">查看紀錄</a>
        </div>
      )
    }
    

    App.jsx 改成 → 導入 Routes

    import { Routes, Route, Link } from "react-router-dom"
    import Home from "./pages/Home"
    import Report from "./pages/Report"
    
    function App() {
      return (
        <>
          {/* 導覽列 */}
          <nav style={{ display: "flex", gap: "12px", padding: "12px" }}>
            <Link to="/">首頁</Link>
            <Link to="/report">紀錄表</Link>
          </nav>
    
          {/* 路由規則 */}
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/report" element={<Report />} />
          </Routes>
        </>
      )
    }
    
    export default App 
    
    • 修改 main.jsx→ 包 BrowserRouter
    import React from "react"
    import ReactDOM from "react-dom/client"
    import { BrowserRouter } from "react-router-dom"
    import App from "./App"
    import "./index.css"
    
    ReactDOM.createRoot(document.getElementById("root")).render(
      <React.StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </React.StrictMode>
    )
    

上一篇
Day24打造一個糖尿病自我監測小工具:從0開始學前端
下一篇
Day26打造一個糖尿病自我監測小工具:從0開始學前端
系列文
打造一個糖尿病自我監測小工具:從0開始學前端26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言