從零開始學習React:瞭解元件的三大屬性

語言: CN / TW / HK

上一節我們簡單介紹了React的相關技術棧,以及如何在一個 html 檔案中使用 React,建立一個React 元件並渲染到Html 中。

本節我們來介紹在 React 中 一個元件比較重要的三大屬性

元件三大屬性

就如我們上一節中介紹的一個最簡單的函式式元件是這樣的:

const H2 = ({ title }) => <h2>{title}</h2>;

這種寫法是比較簡潔的 ES6 寫法,他等同於下面的這種寫法:

function H2(props) {  return <h2>{props.title}</h2>; }

該函式是一個有效的 React 元件,因為它接收唯一帶有資料的 “props”(代表屬性)物件與並返回一個 React 元素。這類元件被稱為“函式元件”,因為它本質上就是 JavaScript 函式。

1. Props

React 除了可以使用原生的 html 標籤,也可以使用使用者自定義的元件:

const title = <H2 title="這是一個自定義元件" />

當 React 元素為使用者自定義元件時,它會將 JSX 所接收的屬性(attributes)以及子元件(children)轉換為單個物件傳遞給元件,這個物件被稱之為 “props”。

當我們有多個值需要傳遞給元件時,在使用元件時,就需要多寫幾個屬性(attributes),例如:

const title = <H2 title="這是一個自定義元件" subTitle="xxx" author="xxxx" />

這樣傳遞似乎有點麻煩,我們還可以使用一個語法糖來簡單的傳遞多個鍵值對的 attributes,:例如:

const props = {title="這是一個自定義元件", subTitle="xxx", author="xxxx"} const title = <H2 {...props} />

注意這並不等同於拷貝物件的 const a = {...b} ,而是 React 的一個語法糖。

props 是隻讀屬性,所以一個函式式元件如果只通過 props 進行渲染,就是一個“純函式元件”,React 非常靈活,但它也有一個嚴格的規則:所有 React 元件都必須像純函式一樣保護它們的 props 不被更改。

當然這種情況很苛刻,在業務中我們很少會使用純函式元件,因為 UI 往往是動態的,而實現動態就需要使用 State。

2. State

state 也就是狀態,在業務中,大多數情況我們的元件都是有狀態的動態元件。例如一個計數器,可以通過 + 增加計數,通過 - 減少計數:

image.png

這是一個非常簡單的示例,在 React 中,我們可以這樣實現:

const Counter = () => {  const [count, setCount] = React.useState(0)  return (    <div>      <h1>{count}</h1>      <button onClick={() => setCount(count + 1)}>+</button>      <button onClick={() => setCount(count - 1)}>-</button>    </div> ) }

useState 是一個hook函式可以讓我們非常方便的使用元件的 State,該函式接受一個值作為初始狀態,返回值是一個元組,第一個值為 state 狀態,第二個值是一個函式,用於更新狀態,這有與 ComposeUI 中的 remember 函式是一樣的,雖然是函式式元件,但是通過 hook,讓函式式元件擁有了狀態,並可以保持狀態。

在 JS 中如果函式的返回值是 元組 型別,我們是可以隨意的對其中的成員進行命名的,但是我們一般推薦在使用 useState 這個hook時,使用 const [state,setState] = useState() 這種命名格式。

不要直接修改 State

行文至此我們需要介紹一個重要的 React 思想就是:不可變,在 React 哲學中我們不應該直接的去改變一個物件的屬性,當需要時,我們應該建立一個新的不同的物件來作為替代。這是 React 中一個重要的思想,很多 API 的設計都是基於此原則。

state 也是一樣,當我們需要修改一個元件的物件時,我們不可以直接操作 state 物件本身,而是需要通過由 useState hook 暴露的的set 函式,傳遞一個新的值作為新的狀態。當 set 函式傳入的新狀態與原有狀態進行對比後,發現需要更新渲染元件,就會觸發 React 對元件的渲染。

//錯誤的寫法 state.name = 'xxxx'; //正確的寫法: setState({...state,name:'xxxx'})

State 的更新可能是非同步的

出於效能考慮,React 可能會把多個 setState() 呼叫合併成一個呼叫。

因為 this.propsthis.state 可能會非同步更新,所以你不要依賴他們的值來更新下一個狀態。

set 函式有兩種方式可以更新狀態:

  1. 直接傳入新值 setCount(123)
  2. 傳入一個函式 setCount(oldState => oldState +1 )

狀態提升

上面的介紹我們可以瞭解到,每個函式元件都可以通過 useState 來獲得狀態,但有時我們需要在多個元件內共享某一個狀態。

還有我們之前寫的計數器作為例子,我們有一個新的元件<IsOdd>,這個元件用於顯示計數器中的數值是否是奇數。我們需要將計數器中的數值狀態共享給判斷元件,這就要用到狀態提升

狀態提升並不是什麼複雜的概念,簡而言之就是將幾個元件需要共享的狀態,從某一個元件,提升到這幾個需要共享狀態的父元件中去。

const App = () => {  //狀態從某個元件提升到了父元件,子元件通過props來讀取狀態、修改狀態  const [count, setCount] = React.useState(0)  return (    <div>      <h1>狀態提升</h1>      <Counter count={count} setCount={setCount} />      <IsOdd count={count} />    </div> ) } //計數器元件不在維護自己狀態,而只通過props接受引數,作為自己元件的成員 const Counter = ({ count, setCount }) => {  return (    <div>      <h1>{count}</h1>      <button onClick={() => setCount(count + 1)}>+</button>      <button onClick={() => setCount(count - 1)}>-</button>    </div> ) } ​ const IsOdd = ({ count }) => {  return (    <div>      <h3>{count % 2 === 0 ? '偶數' : '奇數'}</h3>    </div> ) } // 渲染App ReactDOM.render(<App />, document.getElementById("root"));

單向資料流

不管是父元件或是子元件都無法知道某個元件是有狀態的還是無狀態的,並且它們也並不關心它是函式元件還是 class 元件。

元件可以選擇把它的 state 作為 props 向下傳遞到它的子元件中,子元件會在其 props 中接收引數 count,但是元件本身無法知道它是來自於 App 的 state,還是手動輸入的值。

這通常會被叫做“自上而下”或是“單向”的資料流。任何的 state 總是所屬於特定的元件,而且從該 state 派生的任何資料或 UI 只能影響樹中“低於”它們的元件。

如果你把一個以元件構成的樹想象成一個 props 的資料瀑布的話,那麼每一個元件的 state 就像是在任意一點上給瀑布增加額外的水源,但是它只能向下流動。

3. Refs

相較於前兩個概念,refs 並沒有那麼重要,在需要使用到 refs 的場景一般也都有替換的方案,只有少數場景是必須使用 refs 的。

在典型的 React 資料流中,props 是父元件與子元件互動的唯一方式。要修改一個子元件,你需要使用新的 props 來重新渲染它。但是,在某些情況下,你需要在典型資料流之外強制修改子元件。被修改的子元件可能是一個 React 元件的例項,也可能是一個 DOM 元素。對於這兩種情況,React 都提供瞭解決辦法。

下面是幾個適合使用 refs 的情況:

  • 管理焦點,文字選擇或媒體播放。
  • 觸發強制動畫。
  • 整合第三方 DOM 庫。

避免使用 refs 來做任何可以通過宣告式實現來完成的事情

你可能首先會想到使用 refs 在你的 app 中“讓事情發生”。如果是這種情況,請花一點時間,認真再考慮一下 state 屬性應該被安排在哪個元件層中。通常你會想明白,讓更高的元件層級擁有這個 state,是更恰當的。檢視 狀態提升 以獲取更多有關示例。

在元件內使用 Refs

const Input = () => {  const inputRef = React.useRef()  const handleClick = () => {    alert(inputRef.current.value) }  return (    <div>      <input ref={inputRef} type="text" placeholder="請在此輸入:" />      <button onClick={handleClick} >alert</button>    </div> ) }

在函式式元件中我們使用 useRef 建立一個 ref 容器,通過將 ref 指定給某個 DOM 元素,來完成ref的使用。就像我們在上面說的,這種情況其實完全可以通過“受控元件”來實現,ref 並非唯一解決方案。

訪問 Refs

當 ref 被傳遞給 render 中的元素時,對該節點的引用可以在 ref 的 current 屬性中被訪問。

const node = this.myRef.current;

ref 的值根據節點的型別而有所不同:

  • ref 屬性用於 HTML 元素時,建構函式中使用 React.createRef() 建立的 ref 接收底層 DOM 元素作為其 current 屬性。
  • ref 屬性用於自定義 class 元件時,ref 物件接收元件的掛載例項作為其 current 屬性。
  • 你不能直接在函式元件上使用 ref 屬性,因為他們沒有例項。

Refs 轉發

上面我們說了:你不能直接在函式元件上使用 ref 屬性,也就是說,我們無法簡單的將 ref 作為 props 來傳遞給函式式元件,如果你不信邪,非要將ref 作為 props 傳遞,你會收到如下錯誤:

`` Warning: Input:refis not a prop. Trying to access it will result inundefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://fb.me/react-special-props)

react-dom.development.js:500 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of App. in Input (created by App) in div (created by App) in App ```

但是某些特殊情況下,我們需要在函式元件中使用 ref,這就需要用到 Refs 轉發。通過 React.forwardRef 函式,我們可以輕鬆的將父元件建立的的 ref 容器,轉發給子元件,例如:

``` const FormItem = ({ label, name, children }) => { const inputRef = React.useRef() const handleClick = () => { console.log({ [name]: inputRef.current.value }) } return (

{React.cloneElement(children, { ref: inputRef })}
) } //使用 React.forwardRef 函式轉發Refs const Input = React.forwardRef((props, ref) => { return (
) })

const App = () => { const [count, setCount] = React.useState(0) const ref = React.useRef() return (

Refs轉發

) }

ReactDOM.render(, document.getElementById("root")); ```

通過子元件的轉發,父元件輕鬆拿到了子元件的 DOM 元素,繼而可以實現了父元件對子元件的控制,例如 focus、blur 等操作。

\