李文榆
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # React 讀書會 Session 5 ###### tags: `React` ### 元件每次渲染時 Props 與 State 都是獨立的 * 我們在 session 3 講到 useState 有提到元件每次渲染時 Props 與 State 都是獨立的,是無法改變的我們看以下的例子 ``` import React, { useState, useEffect }from 'react'; export function Counter () { const [count, setCount] = useState(0); useEffect(()=>{ setTimeout(()=>{ console.log(`You clicked ${count} times`); },3000); }); return ( <div> <button onClick={()=> setCount(v => v + 1)}>btn</button> <div> {count} </div> </div> ); } ``` 看到的結果 ![](https://i.imgur.com/lNlCXPR.jpg) 符合預期,的確忠實印出保存在 Counter 每一次渲染時的狀態 但有時候我在每次渲染時希望取得某一次 count 的狀態又或是 count 未來最新的狀態時又該怎麼做? 我們看一下在 React Class Component 裡面我們用同樣的方式來實作上面的效果 ``` import React, {Component} from "react"; import ReactDOM from "react-dom"; export class Counter extends Component { state = { count: 0 }; componentDidUpdate() { setTimeout(() => { console.log(`You clicked ${this.state.count} times`); }, 3000); } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> btn </button> </div> ) } } ``` ![](https://i.imgur.com/0bZjo4e.jpg) 使用 class component 實作時跟你想的不一樣對吧? 每次印出的 count 是最新的狀態 因為這裡的 count 是指向 class component 的實體的成員,並不是 function component 每次 render closure 本身 所以印出 count 是指向同一個物件同一個記憶體,也就是最新的狀態 那麼,我們在 function component 是否能達成類似的效果呢? ### useRef 根據官方的說法 useRef 回傳一個 mutable 的 ref object,.current 屬性被初始為傳入的參數(initialValue)。 回傳的 object 在 component 的生命週期將保持不變。 看不懂上面官方說法沒關係 白話來說 就是讓 function component 的生命週期裡這個物件都是同一個 (同一個記憶體) 不會像是 useState 一樣每次渲染都是不同的物件 然後這個物件下面有個屬性 current 是可以拿來隨時存取的你要用的狀態 其實就是要模擬 class component 中的 instance field 我們用 useRef 來試試看效果 ``` import React, { useState, useEffect, useRef }from 'react'; export function Counter () { const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(()=>{ countRef.current = count; setTimeout(()=>{ console.log(`You clicked ${countRef.current} times`); },3000); }); return ( <div> <button onClick={()=> setCount(v => v + 1)}>btn</button> <div> {count} </div> </div> ); } ``` 看一下結果 ![](https://i.imgur.com/1IjSWaV.jpg) 這邊的行為就跟 class component 裡的行為一致了 透過 current 讓我們可以獲取指定的狀態 不過你可能會問 那我在 function component 外面去定義一個變數不是也是一樣效果? ``` import React, { useState, useEffect, useRef }from 'react'; let countRef = 0; export function Counter () { const [count, setCount] = useState(0); countRef = count; useEffect(()=>{ countRef = count; setTimeout(()=>{ console.log(`You clicked ${countRef} times`); },3000); }); return ( <div> <button onClick={()=> setCount(v => v + 1)}>btn</button> <div> {count} </div> </div> ); } ``` 這樣寫的確會是一樣的效果,但是當我們在多個地方使用到這個元件時,因為這個 countRef 變數是在 function 外部 在不同地方的元件都會共同使用到這個變數,然後就會互改造成錯誤 ### useRef vs useState 一樣都是讓 Function Component 擁有狀態 同樣模擬 class 裡 instance field 的效果 那 useRef 與 useState 有什麼不同? 最大的不同是 useRef 改變值並不會觸發 React 去呼叫重新渲染 useRef 在渲染時都會給你同一個 ref object (同一個記憶體位置) 具體來說,React 元件中想要定義一些變數 但又不想這些變數改變時就像 useState 會重新呼叫被渲染 這時就很適合使用 useRef ### useRef 使用規範與使用情境 由於建構 useRef 的 Ref 物件是貫穿整個元件的生命週期,在任何地方都可以做修改 最常做的就是計算元件的每做一次 lifecycle 的次數 (這邊不講渲染次數的原因後面會講) ``` import React, { useState, useEffect, useRef }from 'react'; export function Counter () { const countRef = useRef(0); return <>{++ countRef.current}</> // 直接在渲染階段裡修改值 ``` 但實際上這種方式並不是 React 建議使用 Ref 的規範 React 建議我們避免在 Render 函數中直接修改 Ref 為什麼這麼說,我們把上面的範例修改一下,刻意按按鈕測試次數 ``` import React, { useState, useEffect, useRef, useReducer }from 'react'; export function Counter () { const countRef = useRef(0); const render = useReducer(state => !state, false)[1]; // 強制Render用的,還不會useReducer沒關係先不管他,後面章節會講 return <div> <button onClick={render}>btn</button> <>{ console.log(++ countRef.current)}</> </div> ``` 我們看看上面的例子跑出來的結果是什麼 ![](https://i.imgur.com/uNjPRWg.jpg) 跟我們預期的完全不同對吧? 我們發現每按一次按鈕就渲染了兩遍 (useRef 很忠實的記錄你渲染次數) 也跟我們想像應該只要渲染一遍的行為不一樣 但這個問題本身並不在於 useRef 而是在於 React 在 16.x 版之後預設加入 React.StrictMode 嚴謹模式 我們看看官網的對於 React.StrictMode 模式的說明 ![](https://i.imgur.com/uTBzkJz.png) 在嚴謹模式下的開發環境裡 React 會幫你執行兩次 render function (但 useEffect 一樣只執行一次) 用意在讓你發現有沒有不正常的 side effect那這問題要怎麼解決呢?(當然最快就是拔掉嚴謹模式) 我們先看看 FunctionComponent 的生命週期 ![](https://i.imgur.com/9g37bBL.png) 這張圖主要有四種階段 Render Phase : Run lazy initializers 與 Render Reconciliation Phase : React Updates DOM Layout Phase : Cleanup LayoutEffectsRun LayoutEffects Commit Phase : Cleanup EffectsRun Effects 這個 Flow 的前兩個綠色階段 Run lazy initializers 與 Render 是屬於 Render Phase 這個階段是不允許做 Side Effects 的,也就是寫有副作用的程式,修改 Ref 就是屬於副作用的操作 這是因為 React 引擎在這個階段隨時可能取消或是重做或是像嚴謹模式執行兩遍 React 建議只有允許副作用的的階段做這件事 只有一個情況是例外的就是 lazy initializers (懶初始化) ``` import React, { useRef } from 'react'; export function Counter () { const countRef = useRef(null); if(countRef.current === null){ countRef.current = 0; } return <div></div> ``` 懶初始的情況下,副作用最多執行一次,而且僅用於初始賦值 這種行為是允許的的特例 如果要讓前面講的計算元件 lifecycle 次數正常運行 我們就必須將有 sideEffect 的修改放到 useEffect 裡去執行 ``` import React, { useState, useEffect, useRef, useReducer }from 'react'; export function Counter () { const countRef = useRef(0); const render = useReducer(state => !state, false)[1]; useEffect(()=>{ countRef.current ++ ; }); return <div> <button onClick={render}>btn</button> <>{ console.log(countRef.current)}</> </div> ``` 結果如下 ![](https://i.imgur.com/y8pMomP.jpg) 這樣就會正常計算每一個 lifecycle 的次數了 另外 useRef 也常被用來參照 DOM 實體 因為每一次渲染都會建出一個新的 DOM 將 DOM 上的 ref 屬性參照給 useRef 實體 透過 useRef 實體可以拿到當下 render 狀態實際的 DOM 物件 並且能夠去控制他們 例如 ``` import React, { useRef } from 'react'; export function Counter () { const inputRef = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputRef.current.focus(); }; return <div> <input ref={inputRef}></input> <button onClick={onButtonClick}>Focus the input</button> </div> ``` 還有一個情境父元件想要存取子元件裡面的 DOM 元素時該怎麼做? 實作概念也很簡單使用情境如下 ``` export const Child = ()=>{ return <> <input/> </> } export const Parent = ()=>{ return <> <Child /> <button>focus child</button> </> } ``` 以上面的範例我希望按下 Parent 元件裡的 button 能讓 Child 元件裡的 input 去 focus 這該怎麼做呢? 首先 Parent 元件裡先定義要存取 Child 的變數 我們使用 useRef 來參照 Child 裡面的元素 把 useRef 當成 props 傳進 Child 裡 ``` export const Parent = ()=>{ const childEl = useRef(null); return <> <Child inputRef={childEl}/> <button>focus child</button> </> } ``` 然後將 Child 元件帶入 props ``` export const Child = ({inputRef})=>{ return <> <input ref={inputRef}/> </> }; ``` 這樣子就能從父元件直接控制子元件裡的 DOM 元素了 ``` export const Parent = ()=>{ const childEl = useRef(null); const onFocus = () => { childEl.current.focus(); // childEl.current就是Child元件裡的input元素 } return <> <Child inputRef={childEl}/> <button onClick={onFocus}>focus child</button> </> } ``` 那如果我希望像是在 class component 那樣使用 ref 去取得整個 Child 呢? 預設上,你不能在 function component 上使用 ref,因為它們沒有 instance 這方式只能在 class component 使用 (使用 createRef 方法) 那在 function component 有沒有方式能夠像在 class component 時使用 ref? ### forwardRef 首先將原本的 Parent 元件裡的 Child 元件上的 prop inputRef 改成使用 ref ``` export const Parent = ()=>{ const childEl = useRef(null); const onFocus = () => { childEl.current.focus(); } return <> <Child ref={childEl}/> <button onClick={onFocus}>focus child</button> </> } ``` 修改 Child 元件 Child 元件必須使用 forwardRef 方法給大家使用 forwardRef 是 React 的 HOC (Higher-Order Components) forwardRef 帶的參數就是 function component 本身 ``` export const Child = forwardRef(()=>{ return <> <input /> </> }); ``` Child 元件本身也要帶兩個參數給父元件 inputData 用 第一個參數就是父元件塞過來的 inputData 第二個參數就是 ref 本身 我們原本父元件帶來的 useRef 是使用 inputData 帶進來的,現在直接使用第二參數 ref ``` export const Child = forwardRef((prop, ref)=>{ return <> <input ref={ref}/> </> }); ``` 當然你可以直接把 ref 參數指定給 input 元素這樣父元件就能完整控制子元件裡 input 所有的功能 ``` export const Parent = ()=>{ const childEl = useRef(null); const onFocus = () => { childEl.current.focus(); } return <> <Child ref={childEl}/> <button onClick={onFocus}>focus child</button> </> } ``` 不過這樣的寫法跟原本父元件 useRef 直接帶 props 進來子元件的方式沒什麼不同 我需要定義 Child 自己的方法給父元件使用 或是限制父元件來取用子元件裡其他 ref 這時該怎麼做? ### useImperativeHandle React 為 function component 提供 useImperativeHandle 鉤子 讓使用 ref 時能向父元件暴露自定義的 instance 值 ``` export const Child = forwardRef((prop, ref)=>{ const inputRef = useRef(null); // 定義給父元件控制的方法 useImperativeHandle(ref, ()=>({ lockOn:()=>{ inputRef.current.focus(); } })); return <> <input ref={inputRef}/> </> }); export const Parent = ()=>{ const childEl = useRef(null); const onFocus = () => { childEl.current.lockOn(); // 使用子元件定義給父元件的方法 } return <> <Child ref={childEl}/> <button onClick={onFocus}>focus child</button> </> } ``` 這樣父元件就能使用子元件定義的方法了 (類似 instance 成員方法) 而使用 useImperativeHandle 方式基本上必須與 forwardRef 搭配使用 以上大概就是使用 useRef 會用到的情境 還有另外一個情境是使用 callBack 當成 ref 這個會在下一個章節 useMemo, useCallBack 講到

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully