當前位置: 妍妍網 > 碼農

React 19 要來了!

2024-04-03碼農

自 2022 年 React v18.2 版本釋出後,React 社群一直翹首以盼新版本的到來。好訊息是,React 團隊正在緊鑼密鼓地籌備,即將推出備受期待的 v19 版本。那麽,React v19 究竟帶來了哪些實用的特性呢?接下來,讓我們一起來探索吧!

React v19 新功能概覽

以下是 React 19 即將帶來的新功能的簡要概述:

  • React編譯器 :React 團隊正在實作一個新的編譯器。目前,Instagram 已經在使用這項技術,它將在未來的React版本中釋出。

  • 伺服端元件 :經過多年的開發,React引入了伺服端元件的概念。現在可以在Next.js中使用這個功能。

  • Actions :Actions 將徹底改變我們與 DOM 元素的互動方式。

  • 文件後設資料 :使開發者能夠用更少的程式碼完成更多工作。

  • 資源載入 :此功能將啟用在後台載入資源,從而改善套用的載入時間和使用者體驗。

  • Web Components :React 程式碼現在將能夠整合 Web Components。

  • 增強的 Hooks :全新 Hook 即將出現,有望徹底改變我們的編碼體驗。

  • React 19 旨在解決 React 長期以來的一個挑戰: 過度重渲染的問題 。以前,開發人員依賴於 useMemo() useCallback() memo 等技術來管理重渲染。新版本將自動處理過度重渲染問題,徹底解放開發者雙手,讓程式碼更簡潔、高效。

    React 編譯器

    目前,React不會自動在狀態改變時重新渲染,這通常需要開發者手動最佳化,如使用 useMemo() useCallback() memo API。然而,React團隊認為這是一種「合理的手動折衷」,並非長久之計,他們的目標是讓 React 自行管理這些重渲染過程。

    為此,他們建立了「React 編譯器」,它將自動決策如何、何時更新狀態和使用者介面,從而徹底解放開發者的雙手。這也意味著開發者不再需要手動使用上述最佳化工具。雖然該功能尚未全面釋出,但已率先在Instagram等生產環境中得到套用,效果顯著。

    React 團隊這一創新舉措,不僅簡化了開發流程,也進一步提升了React的效能和穩定性,令人期待其在未來版本中的全面套用。

    伺服端元件

    如果你尚未聽聞伺服器端元件,那你可能錯過了React和Next.js領域的一大革新。過去,React元件主要執行於客戶端,但如今React正引領一場變革——在伺服端執行元件。

    伺服端元件的概念已流傳多年,而 Next.js 則率先將其套用於生產環境。從Next.js 13開始,所有元件預設都是伺服器端元件,若想讓元件在客戶端執行,只需使用「use client」指令。

    在即將釋出的React 19中,伺服器端元件將直接融入 React 核心,帶來多重優勢:

  • SEO最佳化 :伺服端渲染的元件能向網路爬蟲提供更豐富的內容,進而提升搜尋引擎排名。

  • 效能飛躍 :伺服器=端元件能顯著加快頁面初始載入速度,最佳化整體效能,對內容密集型套用尤為有效。

  • 伺服器端執行 :伺服器=端元件使得程式碼在伺服器端執行成為可能,從而高效處理如API呼叫等任務。

  • 在React中,所有元件預設是客戶端元件,若需轉為伺服器端元件,只需在元件頂部添加「use server」即可。這樣,元件將僅在伺服器端執行,不會涉及客戶端。

    使用伺服端元件非常簡單。可以在任何 React 元件中匯入伺服端元件,並透過「Actions」來執行特定任務。

    'use server';
    exportdefaultasyncfunctionrequestUsername(formData){
    const username = formData.get('username');
    if(canRequest(username)){
    // ...
    return'successful';
    }
    return'failed';
    }

    Actions

    在 React 19 中,另一個激動人心的新功能就是Actions。這將是我們在處理表單時工作方式的一個重大改變。

    Actions 允許開發者將動作與HTML的 <form/> 標簽無縫融合。簡言之,現在可以直接使用Actions替代傳統的 onSubmit 事件。這些動作被巧妙地設計成了HTML表單的內容,讓表單處理更為靈活和高效。

    在Actions之前,我們常依賴於 React 的 onSubmit 事件來處理表單送出,觸發如搜尋等方法的執行。然而,這樣的處理方式通常受限於客戶端,無法在伺服端直接執行相關邏輯。

    <form onSubmit={search}>
    <input name="query"/>
    <button type="submit">Search</button>
    </form>

    但在 Actions 推出後,結合伺服端元件的使用,我們可以輕松地在伺服端執行表單送出動作。在JSX中,不再需要繁瑣的 onSubmit 事件,只需在 <form/> 標簽中使用 action 內容即可。這個內容的值將指向一個方法,負責處理數據的客戶端或伺服端送出。

    更值得一提的是,Actions 不僅支持同步操作,還能輕松應對異步任務,從而極大地簡化了數據送出管理和狀態更新的流程。React 的目標是透過這一創新功能,讓表單處理和數據管理變得更加簡單、直觀。

    下面來透過一個具體範例來深入了解 Actions 是如何工作的:

    "use server"
    constsubmitData=async(userData)=>{
    const newUser ={
    username: userData.get('username'),
    email: userData.get('email')
    }
    console.log(newUser)
    }

    constForm=()=>{
    return<form action={submitData}>
    <div>
    <label>Name</label>
    <input type="text" name='username'/>
    </div>
    <div>
    <label>Name</label>
    <input type="text" name="email"/>
    </div>
    <button type='submit'>Submit</button>
    </form>
    }
    exportdefault Form;

    在上述程式碼中, submitData 是伺服端元件中的動作。 form 是一個客戶端元件,它使用 submitData 作為動作。 submitData 將在伺服端執行。客戶端( form )和伺服端( submitData )元件之間的通訊之所以成為可能,正是得益於 action 內容。

    Web Components

    Web Components 賦予我們能力,使用原生HTML、CSS和JavaScript建立自訂元件,並輕松地將它們融入Web套用中,如同操作原生HTML標簽般自然流暢。

    然而,目前將 Web Components 整合到 React 框架中並非易事。我們通常需要將 Web Components 轉化為React元件,或者安裝額外的庫並編寫額外程式碼來使它們與 React 相容,這無疑增加了開發的復雜度。

    但好訊息是,React 19 將極大地簡化這一過程。未來,當你發現某個實用的 Web Components,如輪播圖元件時,可以輕松地在React計畫中引入它,無需繁瑣的轉換工作。

    這一改進將大大提升開發效率,使我們能夠充分利用現有龐大的 Web Components 生態,為React套用增添更多可能性。

    盡管目前我們尚未得知具體的實作細節,但我期待著它可能帶來的便捷性——或許,我們只需簡單地將 Web Components 匯入 React 計畫,就像模組聯邦那樣。

    文件後設資料

    諸如「標題」、「元標簽」和「描述」等元素在最佳化搜尋引擎最佳化(SEO)和確保可存取性方面起著至關重要的作用。在React中,由於單頁面套用的普及,跨不同路由管理這些元素可能會有點麻煩。

    目前,開發者通常不得不編寫自訂程式碼,或者使用像 react-helmet 這樣的包來處理路由變更並相應更新後設資料。這個過程可能是重復的,並且容易出錯,尤其是在處理像元標簽這樣的SEO敏感元素時。

    之前的方式:

    import React,{ useEffect }from'react';
    constHeadDocument=({ title })=>{
    useEffect(()=>{
    document.title = title;
    const metaDescriptionTag =document.querySelector('meta[name="description"]');
    if(metaDescriptionTag){
    metaDescriptionTag.setAttribute('content','新描述');
    }
    },[title]);
    returnnull;
    };
    exportdefault HeadDocument;

    在上面的程式碼中,有一個名為 HeadDocument 的元件,它負責根據傳入的內容更新標題和元標簽。我們在 useEffect 中執行這些更新。同時,使用JavaScript來更新標題和元標簽。這個元件將在路由更改時更新。這不是一個幹凈的處理方式。

    在React 19中,可以直接在React元件中使用標題和元標簽:

    constHomePage=()=>{
    return(
    <>
    <title>部落格</title>
    <meta name="description"content="部落格"/>
    {/* 頁面內容 */}
    </>
    );
    }

    這在React之前是不可能的。之前唯一的辦法是使用像 react-helmet 這樣的包。React 19的新特性將大大簡化後設資料的管理,使開發者能夠直接在元件內部聲明和更新這些元素,從而提高程式碼的可讀性和可維護性。

    資源載入

    在 React 套用中,有效管理資源載入和效能至關重要,特別是針對圖片和其他資原始檔。

    通常,瀏覽器會先渲染檢視,然後再載入樣式表、字型和圖片。這可能會導致從非樣式化(或未樣式化內容的閃爍)到樣式化檢視的閃爍。為了緩解這個問題,開發者通常會添加自訂程式碼來檢測這些資源何時準備好,確保只在所有內容載入完成後才顯示檢視。

    在 React 19 中,隨著使用者瀏覽當前頁面,圖片和其他檔將在後台載入。這一改進將有助於減少頁面載入時間,降低等待時間。此外,React 19 引入了資源載入的生命周期 Suspense ,包括指令碼、樣式表和字型等。這一新特性允許React精確判斷何時內容已準備完畢,可以安全展示給使用者,從而徹底消除了因資源未載入而導致的頁面閃爍問題。同時,React 19 還會提供 preload preinit 等新的資源載入API,讓開發者對資源何時載入和初始化擁有更精細的控制權。

    透過實作資源的後台異步載入,React 19 極大地減少了使用者的等待時間,讓他們能夠流暢地與頁面內容進行互動。這一重大改進不僅提升了React套用的效能,也為使用者帶來了更加流暢、愉悅的瀏覽體驗。

    增強的 Hooks

    use

    use 是一個實驗性 React Hook,它可以讓讀取類似於 Promise 或 context 的資源的值。

    const value =use(resource);

    官方文件 https://zh-hans.react.dev/reference/react/use

    use(Promise)

    新的 use hook 可以在客戶端進行「掛起」的 API。可以將一個 promise 傳遞給它,React 將會在該 promise 解決之前進行掛起。它的基本語法如下:

    import{ use }from'react';
    functionMessageComponent({ messagePromise }){
    const message =use(messagePromise);
    // ...
    }

    下面來看一個簡單的例子:

    import*as React from'react';
    import{ useState, use, Suspense }from'react';
    import{ faker }from'@faker-js/faker';
    exportconstApp=()=>{
    const[newsPromise, setNewsPromise]=useState(()=>fetchNews());
    consthandleUpdate=()=>{
    fetchNews().then((news)=>{
    setNewsPromise(Promise.resolve(news));
    });
    };
    return(
    <>
    <h3>
    新聞列表
    <button onClick={handleUpdate}>重新整理</button>
    </h3>
    <NewsContainer newsPromise={newsPromise}/>
    </>
    );
    };
    let news =[...newArray(4)].map(()=> faker.lorem.sentence());
    constfetchNews=()=>
    newPromise<string[]>((resolve)=>
    // 使用 setTimeout 模擬數據獲取
    setTimeout(()=>{
    // 每次重新整理時添加一個標題
    news.unshift(faker.lorem.sentence());
    resolve(news);
    },1000)
    );
    constNewsContainer=({ newsPromise })=>(
    <Suspense fallback={<p>請求中...</p>}>
    <News newsPromise={newsPromise}/>
    </Suspense>
    );
    constNews=({ newsPromise })=>{
    const news = use<string[]>(newsPromise);
    return(
    <ul>
    {news.map((title, index)=>(
    <li key={index}>{title}</li>
    ))}
    </ul>
    );
    };

    在上面的例子中,每次重新整理時,都會先顯示「請求中...」,請求到數據後進行展示:

    官方文件中,關於 <Suspense> 有一個警告:

    目前尚不支持在不使用固定框架的情況下進行啟用 Suspense 的數據獲取。實作支持 Suspense 資料來源的要求是不穩定的,也沒有文件。React 將在未來的版本中釋出官方 API,用於與 Suspense 整合資料來源。

    對於 React 19 來說, use 可能就是用於與 Suspense 整合資料來源的官方 API。

    這個全新的 use hook 與其他的 React Hooks 不同,它可以在迴圈和條件語句中像 if 一樣被呼叫。這意味著我們可能不再需要依賴像 TanStack Query 這樣的第三方庫在客戶端進行數據獲取。然而,這仍需進一步觀察,因為 Tanstack Query 的功能遠不止解析 Promise 這麽簡單。

    use(Context)

    這個 use hook 也可以用來讀取 React Context。它與 useContext 作用完全相同,只是可以在迴圈(如 for)和條件語句(如 if)中呼叫。

    import{ use }from'react';
    functionHorizontalRule({ show }){
    if(show){
    const theme =use(ThemeContext);
    return<hr className={theme}/>;
    }
    returnfalse;
    }

    這將簡化某些場景下的元件層級結構,因為在迴圈或條件語句中讀取 context ,之前唯一的方法就是將元件一分為二。

    在效能方面,這一改進也是巨大的進步,因為現在即使 context 發生變化,我們也可以有條件地跳過元件的重新渲染。

    useOptimistic

    useOptimistic Hook 允許在進行送出動作的同時,能夠樂觀地更新使用者介面,提升使用者體驗。其語法如下:

    import{ useOptimistic }from'react';
    functionAppContainer(){
    const[optimisticState, addOptimistic]=useOptimistic(
    state,
    // 更新函式
    (currentState, optimisticValue)=>{
    // 合並並返回帶有樂觀值的新狀態
    },
    );
    }

    樂觀更新 :一種更新應用程式中數據的策略。這種策略通常會先更改前端頁面,然後向伺服器發送請求,如果請求成功,則結束操作;如果請求失敗,則頁面回滾到先前狀態。這種做法可以防止新舊數據之間的跳轉或閃爍,提供更快的使用者體驗。

    下面來看一個添加購物車的例子:

    import{ useState, useOptimistic }from'react';
    constAddToCartForm=({ id, title, addToCart, optimisticAddToCart })=>{
    constformAction=async(formData)=>{
    optimisticAddToCart({ id, title });
    try{
    awaitaddToCart(formData, title);
    }catch(e){
    // 捕獲錯誤
    }
    };
    return(
    <form action={formAction}>
    <h2>{title}</h2>
    <input type="hidden" name="itemID" value={id}/>
    <button type="submit">添加到購物車</button>
    </form>
    );
    };
    type Item ={
    id: string;
    title: string;
    };
    constCart=({ cart }:{cart: Item[]})=>{
    if(cart.length ==0){
    returnnull;
    }
    return(
    <>
    購物車:
    <ul>
    {cart.map((item, index)=>(
    <li key={index}>{item.title}</li>
    ))}
    </ul>
    <hr />
    </>
    );
    };
    exportconstApp=()=>{
    const[cart, setCart]= useState<Item[]>([]);
    const[optimisticCart, optimisticAddToCart]= useOptimistic<Item[], Item>(
    cart,
    (state, item)=>[...state, item]
    );
    constaddToCart=async(formData: FormData, title)=>{
    const id =String(formData.get('itemID'));
    awaitnewPromise((resolve)=>setTimeout(resolve,1000));
    setCart((cart: Item[])=>[...cart,{ id, title }]);
    return{ id };
    };
    return(
    <>
    <Cart cart={optimisticCart}/>
    <AddToCartForm
    id="1"
    title="JavaScript權威指南"
    addToCart={addToCart}
    optimisticAddToCart={optimisticAddToCart}
    />
    <AddToCartForm
    id="2"
    title="JavaScript高級程式設計"
    addToCart={addToCart}
    optimisticAddToCart={optimisticAddToCart}
    />
    </>
    );
    };

    在上面的例子中,將商品添到購物車時,會先在購物車列表看到剛剛添加的商品,而不必等到數據請求完成。這樣,使用者可以更快地看到更新後的購物車內容,提供更加流暢的使用者體驗。

    在介紹 useFormState 之前,先來看以下這個 Hook 使用的背景。

    React 將引入一個新元件: <form> ,它是建立用於送出資訊的互動式控制項,可以將一個函式作為 action 的內容值。當使用者送出表單時,React 將自動呼叫此函式,以執行相應的操作。

    <form action={handleSubmit}/>

    註意,如果在 React 18 中添加 <form action> 內容,就會收到警告:

    ⚠️ Warning: Invalid value for prop action on tag. Either remove it from the element or pass a string or number value to keep it in the DOM.

    這裏的意思是, <form> 標簽上的 prop action 無效。要麽從元素中刪除它,要麽傳遞一個字串或數位值以將其保留在 DOM 中。

    而在新版本中,可以直接在 <form> 標簽上設定 action 內容。例如,在上面的購物車例子中,:

    constAddToCartForm=({ id, title, addToCart })=>{
    constformAction=async(formData)=>{
    try{
    awaitaddToCart(formData, title);
    }catch(e){
    // 捕獲錯誤
    }
    };
    return(
    <form action={formAction}>
    <h2>{title}</h2>
    <input type="hidden" name="itemID" value={id}/>
    <button type="submit">添加到購物車</button>
    </form>
    );
    };

    addToCart 函式並不是在伺服器端執行的,而是在客戶端(例如使用者的瀏覽器)上執行的。這個函式可以是一個異步函式,如網路請求,而不阻止其他程式碼的執行。透過使用 addToCart 函式,開發者可以更簡單地處理React中的AJAX表單,例如在搜尋表單中。然而,這可能還不足以完全擺脫像 React Hook Form 這樣的第三方庫,因為它們不僅處理表單送出,還包括驗證、副作用等多種功能。

    看完這個新功能,下面就來看看這一部份要介紹的新 Hook: useFormState

    useFormState

    useFormState 是一個可以根據某個表單動作的結果更新 state 的 Hook。

    const[state, formAction]=useFormState(fn, initialState);

    只有在表單送出觸發 action 後才會被更新的值,如果該表單沒有被送出,該值會保持傳入的初始值不變。

    例如,這可以用來顯示由表單操作返回的確認訊息或錯誤訊息。

    import{ useState }from'react';
    import{ useFormState }from'react-dom';
    constAddToCartForm=({ id, title, addToCart })=>{
    constaddToCartAction=async(prevState, formData)=>{
    try{
    awaitaddToCart(formData, title);
    return'添加成功';
    }catch(e){
    return"添加失敗:賣完啦";
    }
    };
    const[message, formAction]=useFormState(addToCartAction,null);
    return(
    <form action={formAction}>
    <h2>{title}</h2>
    <input type="hidden" name="itemID" value={id}/>
    <button type="submit">添加到購物車</button>&nbsp;
    {message}
    </form>
    );
    };
    type Item ={
    id: string;
    title: string;
    };
    exportconstApp=()=>{
    const[cart, setCart]= useState<Item[]>([]);
    constaddToCart=async(formData: FormData, title)=>{
    const id =String(formData.get('itemID'));
    awaitnewPromise((resolve)=>setTimeout(resolve,1000));
    if(id ==='1'){
    setCart((cart: Item[])=>[...cart,{ id, title }]);
    }else{
    thrownewError('Unavailable');
    }
    return{ id };
    };
    return(
    <>
    <AddToCartForm
    id="1"
    title="JavaScript權威指南"
    addToCart={addToCart}
    />
    <AddToCartForm
    id="2"
    title="JavaScript高級程式設計"
    addToCart={addToCart}
    />
    </>
    );
    };

    效果如下:

    註意 useFormState 需要從 react-dom 中匯入,而不是從 react 中匯入。

    useFormStatus

    useFormStatus 用於獲取上次表單送出的狀態資訊。

    const{ pending, data, method, action }=useFormStatus();

    它不接收任何參數,會返回一個包含以下內容的 status 物件:

  • pending :布爾值。如果為 true ,則表示父級 <form> 正在等待送出;否則為 false。

  • data :包含父級 <form> 正在送出的數據;如果沒有進行送出或沒有父級 <form> ,它將為 null

  • method :字串,可以是 'get' 或 'post'。表示父級 <form> 使用 GET 或 POST HTTP 方法 進行送出。預設情況下, <form> 將使用 GET 方法,並可以透過 method 內容指定。

  • action :一個傳遞給父級 <form> action 內容的函式參照。如果沒有父級 <form> ,則該內容為 null 。如果在 action 內容上提供了 URI 值,或者未指定 action 內容, status.action 將為 null

  • 下面來繼續看購物車的例子,將商品添加到購物車成功前,禁用添加按鈕:

    import{ useState }from'react';
    import{ useFormStatus }from'react-dom';
    constAddToCartForm=({ id, title, addToCart })=>{
    constformAction=async(formData)=>{
    try{
    awaitaddToCart(formData, title);
    }catch(e){
    // 捕獲錯誤
    }
    };
    return(
    <form action={formAction}>
    <h2>{title}</h2>
    <input type="hidden" name="itemID" value={id}/>
    <SubmitButton />
    </form>
    );
    };
    constSubmitButton=()=>{
    const{ pending }=useFormStatus();
    return(
    <button disabled={pending} type="submit">
    添加到購物車
    </button>
    );
    };
    type Item ={
    id: string;
    title: string;
    };
    constCart=({ cart }:{cart: Item[]})=>{
    if(cart.length ==0){
    returnnull;
    }
    return(
    <>
    購物車:
    <ul>
    {cart.map((item, index)=>(
    <li key={index}>{item.title}</li>
    ))}
    </ul>
    <hr />
    </>
    );
    };
    exportconstApp=()=>{
    const[cart, setCart]= useState<Item[]>([]);
    constaddToCart=async(formData: FormData, title)=>{
    const id =String(formData.get('itemID'));
    awaitnewPromise((resolve)=>setTimeout(resolve,1000));
    setCart((cart: Item[])=>[...cart,{ id, title }]);
    return{ id };
    };
    return(
    <>
    <Cart cart={cart}/>
    <AddToCartForm
    id="1"
    title="JavaScript權威指南"
    addToCart={addToCart}
    />
    <AddToCartForm
    id="2"
    title="JavaScript高級程式設計"
    addToCart={addToCart}
    />
    </>
    );
    };

    添加購物車時效果如下:

    註意 useFormState 需要從 react-dom 中匯入,而不是從 react 中匯入。此外,它僅在父級表單使用 action 內容時才有效。