0%

Enhancing Components with Hooks (Ⅲ)

掛鉤規則

在使用 Hooks 時,有一些原則需要遵守,以避免錯誤和異常的行為:

  • 掛鉤僅能在組件中運行

    掛鉤只能從 React 組件中調用。也可以將它們添加到自定義掛鉤中,然後將它們添加到組件中。 掛鉤不是常規的 JavaScript,它們是一種 React 模式,可建立以模組化方式整合到其他庫中。

  • 將功能分解為多個掛鉤是個好主意

    拆分功能為多個掛鉤,除了使代碼更易於閱讀外,還有另一個好處。由於 Hook 是按順序調用的,因此最好使它們保持較小的功能。調用後,React 會將 Hooks 的值保存在陣列中,所以比較好追蹤較小功能的運作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const Counter = () => {
    const [count, setCount] = useState(0);
    const [checked, toggle] = useState(false);

    useEffect(() => {
    ...
    }, [checked]);

    useEffect(() => {
    ...
    }, []);

    useEffect(() => {
    ...
    }, [count]);

    return ( ...)
    };

    每個 Hook 的調用和渲染順序都相同:

    [count, checked, DependencyArray, DependencyArray, DependencyArray]
  • 掛鉤只能在頂層調用

    掛鉤應在 React 函式的頂層使用。它們不能放入條件語句,循環或嵌套函式中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const Counter = () => {
    const [count, setCount] = useState(0);

    if (count > 5) {
    const [checked, toggle] = useState(false);
    }

    useEffect(() => {
    ...
    });

    if (count > 5) {
    useEffect(() => {
    ...
    });
    }

    useEffect(() => {
    ...
    });

    return ( ... );
    };

    當我們在 if 語句中使用 useState 時,僅當 count 大於 5 時才應調用該掛鉤。拋出的陣列值,有時是 [count, checked, DependencyArray, 0, DependencyArray],有時則會是 [count, DependencyArray, 1]。陣列中的 effect 索引對 React 很重要,這是 React 保存 effect 值的方式。

    這並不是說我們不能在 React 應用程序中使用條件語句。我們只需要以不同的方式組織這些條件。我們可以在掛鉤中嵌套 if 語句,循環和其他條件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const Counter = () => {
    const [count, setCount] = useState(0);
    const [checked, toggle] = useState(count => (count < 5) ? undefined : false);

    useEffect(() => {
    ...
    });

    useEffect(() => {
    if (count < 5) return;
    ...
    });

    useEffect(() => {
    ...
    });

    return ( ... );
    };

    第 3 行,count 小於 5 時,checked 值為 undefined。 將條件嵌套在掛鉤內部而將該掛鉤保留在頂層,但是結果是相似的。 第 9 行的 effect 執行相同的規則,如果 count 小於 5,則 return 語句將阻止 effect 繼續執行。這將使掛鉤陣列值保持一致 [count,checked,DependencyArray,DependencyArray,DependencyArray]

    與條件語句邏輯一樣,當你需要在掛鉤中嵌套異步行為。 useEffect 將函式作為第一個參數,而不是 Promise。因此,您不能將異步函式用作第一個參數:

    1
    useEffect(async () => {})

    但是,你可以在參數函式內部嵌套一個異步函數:

    1
    2
    3
    4
    5
    6
    useEffect(() => {
    const fn = async () => {
    await SomePromise();
    };
    fn();
    });

    我們創建了一個變數 fn,以處理 async/await,然後執行該函式。 您也可以使用匿名函式:

    1
    2
    3
    4
    5
    useEffect(() => {
    (async () => {
    await SomePromise();
    })();
    });

如果遵循上述的這些規則,你可以避免 React Hooks 的一些常見問題。

使用 useReducer 改進代碼

思考一下 Checkbox 複選框組件,該組件擁有簡單的狀態,複選框值只有 “checked” 與 “not checked”。 其中,checked 是狀態值,而 setChecked 是用於更改狀態的函式。 首次渲染組件時,checked 的值為 false:

useReducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div id="react-container" />

<script type="text/babel">
const { render } = ReactDOM;
const { useState } = React;

const Checkbox = () => {
const [checked, setChecked] = useState(false);

return (
<>
<input
type="checkbox"
value={checked}
onChange={() => setChecked(checked => !checked)}
/>
<span className="margin-left-sm">{checked ? "checked" : "not checked"}</span>
</>
);
};

const App = () => (
<>
<h1>Hello Tainan!</h1>
<Checkbox />
</>
);

render(
<App />,
document.getElementById("react-container")
);
</script>

效果不錯,但是此功能如果用在不同的的區域可能會引起一些維護上的困擾。

1
onChange={() => setChecked(checked => !checked)}

乍看下感覺還可以,這裡會有甚麼問題嗎?我們送出一個接受當前 checked 值並返回相反的!checked 的函式。 開發人員有可能會送出錯誤的函式並破壞整個過程。與其以這種方式進行處理,為什麼不提供一個功能函式呢?

讓我們添加一個名為 toggle 的函式,該函式將執行與 setChecked 相同的操作,並返回與 checked 當前值相反的值:

Checkbox and toggle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Checkbox = () => {
const [checked, setChecked] = useState(false);

const toggle = () => setChecked(checked => !checked);

return (
<>
<input
type="checkbox"
value={checked}
onChange={toggle}
/>
<span className="margin-left-sm">{checked ? "checked" : "not checked"}</span>
</>
);
};

這個好些了。onChange 設置為可預測的值:toggle 函式。我們知道每次使用該函式時,它將執行預定地操作,產生更可預測的結果。

這裡我們在 toggle 函式中使用了setChecked 的涵式:

1
setChecked(checked => !checked);

我們現在要使用另一個方式來引用這個函式,checked =>!checked,它是一個 reducer。reducer 函式最簡單的定義是,它接受當前狀態並返回新狀態。 如果 checked 為 false,則應返回相反的 true。

在最初我們將此行為硬編碼為 onChange 事件,我們現在可以將邏輯簡化為 reducer 函式,該邏輯將始終產生相同的結果。

我們將使用 useReducer 代替組件中的 useState:

Checkbox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Checkbox = () => {
const [checked, toggle] = useReducer(checked => !checked, false);

return (
<>
<input
type="checkbox"
value={checked}
onChange={toggle}
/>
<span className="margin-left-sm">{checked ? "checked" : "not checked"}</span>
</>
);
};

第 2 行,useReducer 接受一個 reducer 函式和初始狀態 false。 然後,將 onChange 函式設置為 toggle,這將會調用 reducer 函式。

我們較早的 reducer,checked =>!checked,就是一個很好的例子。如果將相同的輸入提供給函式,則應預期會有相同的輸出。

這個概念起源於 JavaScript 中的 Array.reduce。reduce 從根本上與 reducer 具有相同的作用:它接受一個函式(這個函式會將所有值都簡化為單一的值)和一個初始值,並返回一個單一的值。

Array.reduce 接受一個 reducer 函式和一個初始值。 對於 numbers 陣列中的每個值,將調用 reducer 直到返回一個值:

Array.reduce
1
2
3
const numbers = [28, 34, 67, 68];

numbers.reduce((number, nextNumber) => number + nextNumber, 0); // 197

發送到 Array.reduce 的 reducer 函式接受兩個參數。所以這裡我們也可以將多個參數發送給 useReducer 的 reducer 函式:

Numbers
1
2
3
4
5
6
7
8
const Numbers = () => {
const [number, setNumber] = useReducer(
(number, newNumber) => number + newNumber,
0
);

return <h1 onClick={() => setNumber(30)}>{number}</h1>
};

第 3 行就是這裡的 reducer 函式,接受了兩個參數 number 與 newNumber。在第 7 行調用 setNumber 時就會執行這個 reducer 函式,傳入 30 給 newNumber,然後加到當前的 number 狀態值。現在,每次單擊 h1 時,我們都會將總數加 30。

useReducer 處理複雜狀態

隨著狀態變得越來越複雜,useReducer 可以幫助我們更可預測地處理狀態更新。來看另外一個含有 User 數據的物件:

user data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const firstUser = {
id: "0391-3233-301",
firstName: "Scott",
lastName: "Tiger",
city: "Tainan",
email: "scott@example.com",
admin: false
};

const User = () => {
const [user, setUesr] = useState(firstUser);

return (
<div>
<h1>{user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}</h1>
<p>Email: {user.email}</p>
<p>Location: {user.city}</p>
<button>Make Admin</button>
</div>
);
}

管理狀態時常見的錯誤是覆蓋狀態:

1
2
3
4
5
6
7
8
<button
onClick={(e) => {
e.preventDefault();
setUser({ admin: true });
}}
>
Make Admin
</button>

第 4 行這樣做將覆蓋 firstUser 的狀態,並將其替換為我們發送給 setUser 函式的內容:{admin:true}。這可以透過 JavaScript 的展開運算子(Spread syntax)解決此問題:展開 user 的當前值,然後覆蓋 admin 值:

1
2
3
4
5
6
7
8
<button
onClick={(e) => {
e.preventDefault();
setUser({ ...user, admin: true });
}}
>
Make Admin
</button>

這將採用初始狀態並輸入新的鍵/值:{ admin:true }。我們需要在每個 onClick 中重寫此邏輯,這很容易出錯,也可能會忘記這樣做。現在改用 useReducer:

useReducer
1
2
3
4
5
6
7
const User = () => {
const [user, setUser] = useReducer(
(user, newDetails) => ({ ...user, ...newDetails }),
firstUser
);
...
}

然後,我們發送新的狀態值 newDetails,傳遞給 reducer,並將其推入物件。

1
2
3
4
5
6
7
8
<button
onClick={(e) => {
e.preventDefault();
setUser({ admin: true });
}}
>
Make Admin
</button>

當狀態具有多個子值或下一個狀態取決於上一個狀態時,此模式很有用。