react教學系列

學習React就要學習他現在主流的寫法Hook,學習Hook的好處很多,可以在function中使用state跟一些類似生命週期的React方法,重點是學習的成本相對較低,相較於class的架構,比較簡單容易上手,可讀性也高

useState

這是可以同時存儲值以及觸發渲染機制的功能,一些規則是

  • 宣告的時候要同時設定兩個值,一個是state本身,另一個是更新state的函式
  • 更新的函式通常是set+第一個設定變數
  • useState()裡面參數為”初始值”

首先先簡單寫一個計數器,在沒有加useState的情況下,會發生什麼

  • 先設定一個APP的function組件
  • return一個h1跟button
  • button放一個numAdd的點擊事件,事件可以增加num的數字

我們預期會點擊後更新畫面,但實際上只有num被改變,畫面沒有被渲染

import React, { useState } from "react";

const App: React.FC = () => {
  let num = 0;

  const numAdd = () => {
    num++;
  };
  console.log("num", num);

  return (
    <>
      <h1>計數器{num}</h1>
      <button onClick={numAdd}> +1 </button>
    </>
  );
};

export default App;

};

那是因為需要觸發渲染機制,才會改變畫面,那我們就要使用useState來讓變數的更動,觸發渲染機制

  • 把useState給import進來
  • 設定初始的useState值
  • 將numAdd的方法修改為State修改值的方法

就可以發現,點擊後的數字修改,也會讓畫面上的數字跟著修改

import React, { useState } from "react";

const App: React.FC = () => {
  //   let num = 0;
  const [num, SetNum] = useState(0);
  const numAdd = () => {
    SetNum(num + 1);
  };
  console.log("num", num);
  return (
    <>
      <h1>計數器{num}</h1>
      <button onClick={numAdd}> +1 </button>
    </>
  );
};

export default App;

這邊再提到一個重要的觀念變數渲染的作用範圍,實務上比較容易遇到,至於影片*中提到的批量更新的部分就不記錄了

※影片是指 布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI

 的課程

這裡拿剛剛的例子,在numAdd這裡面再新增一次+1,我們期望可以點擊一次,觸發兩次,但實際上,他只會運作一次

const App: React.FC = () => {
  //   let num = 0;
  const [num, SetNum] = useState(0);
  const numAdd = () => {
    SetNum(num + 1);
    SetNum(num + 1);
  };

  console.log("num", num);
  return (
    <>
      <h1>計數器{num}</h1>

      <button onClick={numAdd}> +1 </button>
    </>
  );
};

export default App;

這裡把他修改為取得上一個數字,並+1回傳,這樣得情況下,才可以達成我們要的在同一個作用下渲染兩次

import React, { useState } from "react";

const App: React.FC = () => {
  //   let num = 0;
  const [num, SetNum] = useState(0);
  const numAdd = () => {
    const newNum = (prev: number): number => {
      return prev + 1;
    };

    SetNum(newNum);
    SetNum(newNum);
  };

  console.log("num", num);
  return (
    <>
      <h1>計數器{num}</h1>

      <button onClick={numAdd}> +1 </button>
    </>
  );
};

export default App;

useEffect

跟useState很像,當改變設定的值時,會觸發渲染,但同時也會觸發本身帶的方法,而不是像useState改變變數

  • 宣告時,設定時為一個方法
  • 參數一是自帶一個方法,當觸發時會被執行
  • 參數二是一個陣列,裡面放要監聽的變數

繼續拿之前的計數器,加上奇偶數的判斷,就要在useEffect中加上加入這些判斷式

  • 將useEffect給import進來
  • 先設定儲存奇偶數的state
  • return一個p標籤來顯示奇偶數
  • 宣告一個useEffect並且將num數字變化給放入參數2

所以每當數字改變時,就會判斷是奇、偶數

import React, { useState, useEffect } from "react";

const App = () => {
  const [num, setNum] = useState(0);
  const [test, settest] = useState("偶數");

  useEffect(() => {
    if (num % 2 === 0) {
      settest("偶數");
    } else {
      settest("奇數");
    }
  }, [num]);
  const numAdd = () => {
    setNum(num + 1);
  };
  return (
    <>
      <h1>計數器:{num}</h1>
      <button onClick={numAdd}>+1</button>
      <p>{test}</p>
    </>
  );
};

export default App;

模擬生命週期componentDidMount()

要知道fnnction是沒有生命週期的,用這方式,是讓function可以透過useEffect來模擬生命週期的感覺,那就是將參數二的陣列不要放值即可,這樣就只有一開始渲染時會執行一次,這裡面可以放一開始渲染時要抓的內容(一些需要抓後端的內容)

import React, { useState, useEffect } from "react";

const App = () => {
  useEffect(() => {
    console.log("hello");
  }, []);

  return (
    <>
    </>
  );
};

export default App;

retrun功能

useEffect的return呢,重新渲染useEffect本身之前,會執行的一個動作,而是重新渲染,那就不包含第一次渲染

以下範例是,點擊按鈕觸發useState的重新渲染,並且觀察重新渲染的effect是return比原本的執行內容還要先跑

結果就是第一次渲染沒有執行return,重新渲染後,就會開始執行return

import React, { useState, useEffect } from "react";

const App = () => {
  const [num, setNum] = useState(0);
  console.log("num", num);
  useEffect(() => {
    if (num == 0) {
      console.log("第一次useEffect執行");
    } else {
      console.log("useEffect執行");
    }
    return () => {
      console.log("useEffect執行return");
    };
  }, [num]);

  return (
    <>
      <button
        onClick={() => {
          setNum(num + 1);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

export default App;

useRef

有點類似useState,但他不涉及觸發渲染,也就是說只有保存值的功能,聽起來一般變數也能做到,但實際變數在使用時,可能會因為重新渲染被覆蓋掉

我們設定一個變數跟useState,兩個同時被點擊後會+1,然後重新渲染被印出來,但得到的結果num都是0,是因為每次num都被重新渲染後,重新宣告為0,而且useState每次都會觸發渲染,我們並不想要渲染效果

import React, { useRef, useState } from "react";
const App: React.FC = () => {
  let num: number = 0;
  const [numState, setNumState] = useState(0);
  console.log("num", num, "numState", numState);
  const clickNum = () => {
    num++;
    setNumState(numState + 1);
  };
  return (
    <>
      <button onClick={clickNum}> + 1 </button>
    </>
  );
};

export default App;

換成用useRef的時候,就是不會觸發渲染,可以在裡面繼續運算著+1,直到5的時候觸發useState渲染之後,也不會歸零,一樣可以持續剛剛的數字繼續往下加

import React, { useRef, useState } from "react";
const App: React.FC = () => {
  const numRef = useRef(0);
  const [hidden, setHidden] = useState(false);

  const clickNum = () => {
    numRef.current++;
    console.log(numRef.current);
    if (numRef.current == 5) setHidden(true);
  };
  return (
    <>
      <button onClick={clickNum}> + 1 </button>
      {hidden && <p>隱藏區塊</p>}
    </>
  );
};

export default App;

useContext

之前學過的props是把父組件的值,傳遞到子組件上面去使用,但如果今天組件非常多層的時候,最上層要傳到最下層,只使用props的時候,一層一層傳下去造成非常麻煩的過程,程式碼也很亂,這時候就可以使用useContext

  • 首先把useContext跟createContext從react給import進來
  • 先建立createContext,並且給予他一個預設值
    • createContext的預設值是,當拿到這個值,卻不是在本身子組件內,就會達到預設值
  • 建立一個組件,並且參數為children(預設的props)
  • retrun的部分是將上面創立的createContext有預設Provider的組件
    • value值則是真正會被傳遞下去的值,如果不是子組件拿到createContext就是拿到預設
  • 下面再建立一個自訂義的hook把useContext設定好,是用來取的值用的

※2023/02/23更新:children必須賦予型態

import React, { useContext, createContext, useState } from "react"; 

const numDefult:number =0
const BtnContext = createContext(numDefult);
 
type Props  ={ 
  children: React.ReactNode; //👈 children prop typr
};

export const BtnProvider = (prop:Props ) => {
  const [num, setNum] = useState(1);
  const clickAdd=()=>{
    setNum(num+1)
  }
  return (
    <>
      <BtnContext.Provider value={num}>
        {prop.children}
        <button onClick={clickAdd}>+1</button>
      </BtnContext.Provider>
    </>
  );
};
 
export const useBtnContext = () => {
  return useContext(BtnContext);
};

要使用剛剛設定的useContext先回到原本有很多層組件的檔案,這邊設定app包app1包app2,所以我想讓app2拿到最上層的值

  • 將剛剛設定的組件還有hook給import進來
  • 把設定好的BtnProvider組件包覆在要傳遞的子組件(app1)
    • 這樣app1跟app2都是useContext的範圍
  • 那這裡使用設定的hook,就能取得我需要的值
import React from "react";
import { useBtnContext, BtnProvider } from "./context";

const APP2: React.FC = () => {
  const num = useBtnContext();
  return (
    <>
      <p>這是app2</p>
      <p>現在數字為 {num}</p>
    </>
  );
};

const APP1: React.FC = () => {
  return (
    <>
      <p>這是app1</p>
      <APP2 />
    </>
  );
};

const App: React.FC = () => {
  return (
    <>
      <BtnProvider>
        <APP1 />
      </BtnProvider>
    </>
  );
};
export default App;

複習JS的比較

我們透過”===”可以比較兩者是否為同等型態及同樣的值,但對於物件、陣列、函式來說,比的是”記憶體位置”,也就是他們是不是完全同一個地方出來的

從下面這幾行程式碼知道,物件、陣列、函式比較的並非是長的一不一樣,而是存取的位置在哪裡

    //true
    console.log("數字比較", 1000 === 1000);
    //false
    console.log("陣列比較", [] === []);
     //false
    console.log("物件比較", {} === {});
     //false
    console.log("函式比較", (() => {}) === (() => {}));

useMemo

是用來記憶一個值,而記憶本身是為了不做無謂的重新渲染,以及一些行為,減少效能的消耗,讓速度提升

宣告方式

  • 先import進來
  • 宣告時,參數一是一個帶有回傳的function,如果不帶回傳就是undefind
  • 宣告時,參數一是跟useEffect一樣,帶入監聽的內容

從這個範例可以看到,如果我們一般宣告useEffect,參數二沒監聽內容,就只會執行一次,不管重新渲染幾次都一樣

import React, { useEffect, useState, useMemo, useCallback } from "react";
const App = () => {
 const [value, setValue] = useState(false);

  console.log("重新渲染");
  useEffect(() => {
    console.log("載入後端資料");
  }, []);

  return (
    <>
      <button
        onClick={() => {
          setValue(!value);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

export default App;

但如果今天我們需要監聽每個物件,來更新後端的資料,就會造成一旦不相干的畫面更新,也會影響到useEffect的內容,因為上述物件比較,每次物件的位置都被重新渲染時重新設定了

import React, { useEffect, useState, useMemo, useCallback } from "react";

const App = () => {
 const [value, setValue] = useState(false);

  console.log("重新渲染");

  const relodding = {name :'hank'};

  useEffect(() => {
    console.log("載入後端資料");
  }, []);

  return (
    <>
      <button
        onClick={() => {
          setValue(!value);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

這時候使用useMemo來記憶物件的內容,並且只要宣告後,物件本身的內容沒有改變,就不會useEffect重新被渲染

import React, { useEffect, useState, useMemo, useCallback } from "react";

const App = () => {
  const [value, setValue] = useState(false);

  console.log("重新渲染");

  //   const relodding = { name: "hank" };
  const relodding = useMemo(() => {
    return { name: "hank" };
  }, []);


  useEffect(() => {
    console.log("載入後端資料");
  }, [relodding]);

  return (
    <>
      <button
        onClick={() => {
          setValue(!value);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

export default App;

useCallback

功能跟剛剛useMemo差不多,只是他是自帶function,就不能做其他事,那useMemo則是回傳一個值,但同樣可以做到回傳function

宣告的話

  • 先import進來
  • 宣告時,參數一是只能放入function,並且就是自帶回傳這個function
  • 宣告時,參數一是跟useEffect一樣,帶入監聽的內容

先從剛剛的例子改成Memo回傳function

import React, { useEffect, useState, useMemo, useCallback } from "react";

const App = () => {
  const [value, setValue] = useState(false);

  console.log("重新渲染");

    const relodding = useMemo(() => {
        return ()=>{}
    }, []);

  useEffect(() => {
    console.log("載入後端資料");
  }, [relodding]);

  return (
    <>
      <button
        onClick={() => {
          setValue(!value);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

export default App;

那其實useCallback可以做到一樣的效果,只是他自己本身就是帶fucntion,所以不會再回傳function

import React, { useEffect, useState, useMemo, useCallback } from "react";

const App = () => {
 
  const [value, setValue] = useState(false);

  console.log("重新渲染");

  const relodding = useCallback(() => {}, []);

  useEffect(() => {
    console.log("載入後端資料");
  }, [relodding]);

  return (
    <>
      <button
        onClick={() => {
          setValue(!value);
        }}
      >
        重新渲染
      </button>
    </>
  );
};

export default App;

memo

當組件有上下層關係的時候,只要父層重新渲染,子層的組件也會重新渲染,但其實變更的內容只有父層的時候,子層的渲染就是多餘的,避免這種浪費效能的狀況,就可以使用memo

宣告方式

  • 先import進來
  • 直接將組件等號後面的部分全部都包起來

從範例中可以看到,只要點擊父層的按鈕,重新渲染後,子層的組件也會被重新渲染

import React, { useState, useMemo, memo } from "react";

const App1: React.FC = () => {
  console.log("子層渲染");

  return (
    <>
      <p>子層</p>
    </>
  );
};

const App: React.FC = () => {
  console.log("父層渲染");
  const [relodding, setRelodding] = useState(false);

  return (
    <>
  
      <App1 />
      <button
        onClick={() => {
          setRelodding(!relodding);
          console.log("---點擊重新渲染---");
        }}
      >
        重新渲染
      </button>

    </>
  );
};

export default App;

加入meno就不會有這個問題,他會判斷是否子組件有發生變動,有的話才會進行渲染

import React, { useState, useMemo, memo } from "react";

const App1: React.FC = memo(() => {
  console.log("子層渲染");

  return (
    <>
      <p>子層</p>
    </>
  );
});

const App: React.FC = () => {
  console.log("父層渲染");
  const [relodding, setRelodding] = useState(false);

  return (
    <>
   
      <App1 />
      <button
        onClick={() => {
          setRelodding(!relodding);
          console.log("---點擊重新渲染---");
        }}
      >
        重新渲染
      </button>

    </>
  );
};

export default App;

加入props之後,當數字發生變化,也代表子組件變化,所以當變化完之後,不在變化時,就不會繼續渲染,只會渲染發生變化的那次

import React, { useState, useMemo, memo } from "react";

type prop = {
  num: number;
};

const App1: React.FC<prop> = memo(({ num }) => {
  console.log("子層渲染");

  return (
    <>
      <p>子層</p>
      <p>數字{num}</p>
    </>
  );
});

const App: React.FC = () => {
  console.log("父層渲染");
  const [relodding, setRelodding] = useState(false);
  const [num, setNum] = useState(0);

  return (
    <>
      <App1 num={num} />
      <button
        onClick={() => {
          setRelodding(!relodding);
          console.log("---點擊重新渲染---");
        }}
      >
        重新渲染
      </button>

      <button
        onClick={() => {
          setNum(100);
          console.log("---點擊數字---");
        }}
      >
        數字
      </button>
    
    </>
  );
};

export default App;

memo結合useMemo的使用

從這個範例中,我們可以知道當傳送過去的東西變成物件之後,又開始不斷刷新子組件,問題還是在於,物件的記憶體位置發生變化,所以這時候就可以加入useMemo來解決這個問題

import React, { useState, useMemo, memo } from "react";

type prop = {
  num: number;
  obj: { name: string };
};

const App1: React.FC<prop> = memo(({ num, obj }) => {
  console.log("子層渲染");

  return (
    <>
      <p>子層</p>
      <p>數字{num}</p>
      <p>名子{obj.name}</p>
    </>
  );
});


const App: React.FC = () => {
  console.log("父層渲染");
  const [relodding, setRelodding] = useState(false);
  const [num, setNum] = useState(0);
  const [obj, setObj] = useState({ name: "?" });

  return (
    <>
      <App1 num={num} obj={obj} />
      <button
        onClick={() => {
          setRelodding(!relodding);
          console.log("---點擊重新渲染---");
        }}
      >
        重新渲染
      </button>

      <button
        onClick={() => {
          setNum(100);
          console.log("---點擊數字---");
        }}
      >
        數字
      </button>

      <button
        onClick={() => {
          setObj({ name: "hank" });
          console.log("---點擊姓名---");
        }}
      >
        姓名
      </button>
    </>
  );
};

export default App;

中間再加入useMemo的回傳,這樣就可以判定是不是有發生值的變動,而不是記憶體位置的變動

import React, { useState, useMemo, memo } from "react";

type prop = {
  num: number;
  obj: { name: string };
};

const App1: React.FC<prop> = memo(({ num, obj }) => {
  console.log("子層渲染");

  return (
    <>
      <p>子層</p>
      <p>數字{num}</p>
      <p>名子{obj.name}</p>
    </>
  );
});

const App: React.FC = () => {
  console.log("父層渲染");
  const [relodding, setRelodding] = useState(false);
  const [num, setNum] = useState(0);
  const [obj, setObj] = useState({ name: "?" });

  const objMemo = useMemo(() => {
    return { name: "hank" };
  }, []);

  return (
    <>
      <App1 num={num} obj={obj} />
      <button
        onClick={() => {
          setRelodding(!relodding);
          console.log("---點擊重新渲染---");
        }}
      >
        重新渲染
      </button>

      <button
        onClick={() => {
          setNum(100);
          console.log("---點擊數字---");
        }}
      >
        數字
      </button>

      <button
        onClick={() => {
          setObj(objMemo);
          console.log("---點擊姓名---");
        }}
      >
        姓名
      </button>
    </>
  );
};

export default App;

結論

今天持續學習布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI,基本上React Hook的觀念我之前有學過一輪,但完全忘記怎麼使用,畢竟當初學也沒做紀錄,也一段時間都沒接觸React,但透過課程學習之後,確實讓我印象深刻的是,布魯斯會說明每個Hook為什麼會被這樣使用及設計,不單單只是教怎麼使用而已,而是讓學的能夠理解原理,更能清楚使用的時機,以及跟過去存在的比較,讓觀念釐清之後,比較好記憶跟使用,所以比起之前看書自學,這次一輪的學習讓我對這些Hook的用法跟觀念都更上一層

布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI

By dong

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *