ui.dev 上的 React Hooks 課程筆記
學習一個新的工具或概念前要先明白,這個工具或概念解決了什麼問題。要了解 React Hooks 解決什麼問題之前,要先認識 how we’ve historically written React components.
React.createClass()
React 在第一次發表後,因為當時 JavaScript 還不具有 built-in (內建的) class 語法,React.createClass()
was the first way in which we created React components. 。
React.Component
在 ES6 之後 JavaScript 開始能使用 Class 語法,React 便從 v0.13.0 之後開始提供 React.Component
API,支援在 React 裡透過 JavaScript 原生的 class 語法定義組件。
constructor()
- 在利用 class 建立組件時,state 是組件實例上的一個屬性。在 constructor()
裡初始化 state。根據 ECMAScript,子類的 constructor method 要先調用 super()
才能使用關鍵字 this
。React 裡面的 class 組件都繼承 React.Component
,所以在 React 裡面以 class 定義組件時,若要為組件添加 state,就必須使用到關鍵字 this
,要使用 this
之前要先調用 super()
,因為都是 React.Component
的子類。若有 props
則必須傳入 props
。
Autobinding - 利用 class 定義組件時,若 method 需要用到關鍵字 this
,必須調用 .bind(this)
Cannot read property setState of undefined
沒有 bind 的情況下會收到的 error message
Class fields alow you to add instance properties directly as a property on a class without having to use constructor. 不需要再透過 constructor 為實例添加屬性 。
有了 class fields,以 class 定義組件的缺點就被解決了。We no longer need to use constructor()
to set the initial state of the component and we no longer need to .bind()
in the constructor since we could use arrow functions for our methods.
以 Class Fields 定義的 React 組件 (我其實還沒有很懂 Class Fields 是怎麼運作的)
class ReposGrid extends React.Component {
state = {
repos: [],
loading: true
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
const { loading, repos } = this.state
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
以上這些都只是以 class 定義組件的小問題,有其他更深入的問題存在。例如
React 的組件有生命週期,而為一個組件定義 custom logic 的時候這個 logic 必須跟生命週期綁在一起,how we’ve structured our React components has been coupled to the component’s lifecycle. 這會造成相關的邏輯步驟被分散在不同的生命週期函數裡。This divide naturally forces us to sprinkle related logic throughout the component.
React 組件的三個部分
我自己把 React 組件看成三個部分:state, custom logic, UI
// We need three separate methods (componentDidMount, componentDidUpdate, and updateRepos)
// to accomplish the same thing - keep repos in sync with whatever props.id is.
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos = (id) => {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
React 是一個用來 create UI 的 library,但 building a front-end 並不只是包括 UI 這個層面,也包含了與 UI 有關的 non-visual logic。相同的邏輯可能會在不同的組件上重複。為了重複利用組件之間相同的程式碼 (減少組件之間的重複邏輯),可以使用 Higher-order Component (高階組件),若組件之間有相同的邏輯架構,可以把這個相同的邏輯架構抽離出來放到高階組件中,重複的邏輯被定義在高階組件返回的組件中。透過高階組件的包裝,將相同的邏輯重複利用在不同的組件上。包裝組件與被包裝組件之間透過 props
傳遞數據
但是透過高階組件減少組件之間重複邏輯的方式,有可能會因為有太多層的高階組件而形成 wrapper hell 而使得程式碼不容易閱讀理解。
export default withHover(
withTheme(
withAuth(
withRepos(Profile)
)
)
)
到目前為止,以 class 建立組件有幾個缺點
React.Component
所以要使用 this
前都必須要在 constructor()
內調用 super(props)
this
必須要 bind所以 React 提供了新的 component API 來建立組件並解決上述的問題。
Since React v0.14.0, we’ve had two ways to create components
The difference was that if our component had state or needed to utilize a lifecycle method, we had to use a class. Otherwise, if it just accepted props and rendered some UI, we could use a function. **在組件需要 local state, custom logic, 或調用生命週期函數的時候,以 class 定義組件。若組件本身只負責接受 props 為參數並返回一個 JSX。以 function 定義組件即可。**而 React Hooks 這個 component API 允許只以 function 定義組件,即便組件需要 local state, custom logic, 或調用生命週期函數的時候。(我的理解)
以 function 取代 class 定義組件的好處
super(props)
我覺得重點是 React Hooks 讓 function component 能夠做到 class component 做的事 (local state, custom logic, 可以調用生命週期函數),且又解決了使用 class component 的缺點
super(props)
this
在任何時候都能以 function component 定義組件。
useState()
- 為 function component 添加 local state簡單介紹第一個 Hook
useState()
接受一個 state 的初始值 (initial value) 為參數,並返回一個 array。array 的第一個元素為 state value,第二個元素為負責修改 state 的函數,稱為 updater function。
const loadingTuple = React.useState(true)
const loading = loadingTuple[0]
const setLoading = loadingTuple[1]
...
loading // true
setLoading(false)
loading // false
為了讓程式碼看起來更簡潔,Typically, you’d use Array Destructuring to grab the values in one line.
const [ loading, setLoading ] = React.useState(true)
// 拿以前 class component 的方式做對照
const [ 儲存 state 的變數, 修改 state 的函數 ] = React.useState(state 初始值)
定義一個 ReposGrid
component
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
useEffect()
- 取代 class component 的 lifecycle methods組件生命週期函數造成的問題 - Sprinkling related logic throughout the component. 在 React Hooks 裡利用 synchronization 取代了 class component 的 lifecycle methods 的概念。
通常在 class component 裡需要調用 lifecycle methods 的時候 Whether it was to set the initial state of the component, fetch data, update the DOM, anything - the end goal was always synchronization. 目的都是為了 synchronization。 Typically, synchronizing something outside of React land (an API request, the DOM, etc.) with something inside of React land (component state) or vice versa.
所以為了解決使用 lifecycle events 的缺點,不再以組件的生命週期這個角度來解決 synchronization,而是以 synchronization 的角度解決 synchronization。透過 useEffect()
這個 Hook 來以 synchronization 的角度實現 synchronization.
useEffect()
接受一個 function 與一個 optional array 為參數,函數定義要執行的 effect (synchronization),optional array 定義什麼時候再次調用這個,當 array 裡的變數改變時,重新調用 effect。
React.useEffect(() => {
document.title = `Hello, ${username}`
}, [username])
上述例子,被當作參數傳入的函數會在 username
改變時被調用,所以每當 username
改變,doucment.title
就會同步為 Hello, ${username}
。(所以 optional array 定義什麼時候再次調用這個 effect 是類似利用監聽某一個變數變化的意思嗎?)
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
// 試著理解看看這裡的 useEffect 負責做什麼?
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
在 React 裡 React couples UI to a component。 這造成了 class component 必須透過 Higher-order components 重複利用組件之間的 non-visual logic。透過 Custom Hooks that are decoupled from any UI 實現 Non-visual logic,function component 就可以透過調用 Custom Hooks 以重複利用 function component 之間的重複邏輯。
我的理解是,Hooks 其實就是 function,利用 Custom Hooks 來負責處理組件之間重複的 non-visual logic。相關連的邏輯被抽離出來由一個 Custom Hook 負責。
例如以下的這個 Custom Hooks useRepos()
,這個 Hooks 接受一個 id
為參數,當 id
改變時,會以新的 id
fetch Repos,並返回一個裝有 loading
與 repos
state 的 array。
function useRepos (id) {
// 我的理解是,看起來組件的 state 與 UI 是分開的,repos, loading 是在 custom hooks 裡被 initialized
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
return [ loading, repos ]
}
如此一來無論是哪一個組件,甚至是 non-visual logic,只要需要執行透過 id
更新 repos 的邏輯,就可以調用 useRepos
custom hook
function ReposGrid ({ id }) {
const [ loading, repos ] = useRepos(id)
...
}
function Profile ({ user }) {
const [ loading, repos ] = useRepos(user.id)
...
}
我的理解
this.state
-> useState()
useEffect()
React.useState()
- Managing State with Hooks一個前端的 App 可以看成由兩個部分組成
在 React 裡,UI 由組件組成,而組件擁有並管理自己的 State。
Perhaps the most critical part of React is the ability for individual components to own and manage their own state.
在 React 中建立組件的方式有兩種
在 React Hooks 出現之前,只有 Class Component 可以擁有並管理自己的 State。
state
屬性,state
的值是一個物件,用來儲存組件所有的 Local State。setState()
更新 State,組件會重新渲染。在 React Hooks API 推出之後,Function Component 透過調用 React.useState()
為組件添加 Local State。
React.useState()
被調用時接受組件一個 State 的初始值為參數,返回一個 Array。
React.useState
takes in a single argument, the initial value for that piece of state, and returns an array with the first item being the state value and the second item being a way to update that state.
所以本來在 Class Component 中,組件所有的數據狀態都定義在 state
這個屬性上。但在 Function Component 上調用 useState()
只為組件添加 One Piece of State,所以每一個數據狀態分開來調用 useState()
,並且會有各自的 Updater。
實務上會利用 Destructing 將 useState()
返回的 Array 裡的值賦值給變數。變數命名原則為
[state 名稱, set{state 名稱}]
function Theme () {
const [theme, setTheme] = React.useState('light')
// 使用解構賦值
// 注意命名習慣,state 名稱、set{state 名稱}
const toDark = () => setTheme('dark')
const toLight = () => setTheme('light')
return (
<div className={theme}>
{theme === "light"
? <button onClick={toDark}>🔦</button>
: <button onClick={toLight}>💡</button>}
</div>
)
}
useState()
- 理解 useState()
的功能Preserving values between renders (Perserve Values) - 在 JavaScript 裡,沒有使用 Closure 的情況下,當一個函數執行完畢後,函數內部定義的變數是無法再被存取的。每次調用同一個函數,會產生各自獨立的內部變數。函數內部定義的變數在每一次調用間沒有關係。所以如果以這樣的觀念來思考 React 裡的 Function Component,Function Component 的 State 其實就是函數內部定義的一個變數,當組件的 State 被改變要重新渲染時,應該會產生另一個獨立的 State。但在 React 裡,有其機制在 Function Component 每次渲染之間保存函數內部的 State。這個機制的實現就是 useState()
。useState()
會在每次渲染之間保留組件的數據狀態。
Typically when you invoke a function in JavaScript, unless you’re utilizing closures, you expect any values defined in that function to get garbage collected once the function is finished executing and you expect each subsequent call to that function to produce its own unique values.
The whole point of React is that components are able to describe their UI based on their current (即便函數執行完畢,仍能存取到目前的 State) state, View = fn(state). This implies that React, under the hood, has some way to preserve values between function calls to prevent them from being garbage collected once the function has finished executing.
Updating the state and triggering a component re-renders (Trigger Re-renders) - 調用 useState()
回傳的 Updater Function 就像調用 setState()
,Updater Function 接受新的 State Value 為參數,若新的 State Value 與當前的 State Value 不一致,則 Updater Function 會更新 State 並重新渲染組件。
總和起來,useState()
is the tool to preserve values between function calls/renders and to trigger a re-render of the component.
JavaScript Garbage Collection (垃圾回收機制)
(尚未補充) 我的理解是這樣的,在 JavaScript 裡,當一個函數執行完畢,Function Execution Context 離開 Execution Stack 後,所有資源會被釋放,也就是說應該存取不到這個函數裡的變數。
React Hooks -
React.useRef()
Allows you to preserve values between renders but, unlike useState, won’t trigger a re-render to the component. 只能保留值,不能用來更新 State 與觸發重新渲染組件。
state
屬性設置 State 初始值,而是每一個 State 都必須各自調用 useState()
setState()
更新 State,而是每一個 State 有自己的 Updater Function在 Class Compnent 裡,組件的所有數據狀態都儲存在 this.state
這個物件上,並且透過調用 setState()
來更新 this.state
,setState()
接受一個新的 State 物件為參數,且只需要傳入要更新的部分。這代表 React 會 Merge 新與舊的 State 產生更新後的 State。
state = {
loading: true,
authed: false,
repos: []
}
setLoading = (loading) => {
this.setState({
loading
}) // wouldn't modify "authed" or "repos".
}
在 Function Component 裡調用 useState()
回傳的 Updater Function 更新 State,Updater Function 接受一個新的 State Value 為參數,且是以新的 State 取代舊的 State,useState()
並不會將新與舊的 State 合併。所以若是 State 的資料型態是一個物件的話,因為 useState()
是以新的 State 取代舊的 State,並需傳入完整的物件。
若為了簡化程式碼,可以使用 Spread Operator 將 Shallow Copy, 覆寫, 與擴增物件。但有可能造成 Performance Hit。
const setLoading = (loading) => {
setState({
...state,
loading
})
}
若是 State 的資料型態是一個物件的話,更適合使用 useReducer()
。
在 Class Component 裡調用 setState()
時,若要根據舊的 State 修改得到新的 State,會將函數當作參數傳入 setState()
,被當作參數傳入的函數會自動被傳入舊的 State 為參數,並返回一個代表新的 State 的物件,一樣只需要返回要更新的部分。
在 Function Component 裡使用 useState()
時,若也要根據舊的 State 修改成新的 State,也是將一個函數當作參數傳入 Updater Function。被當作參數傳入的函數會自動被傳入舊的 State 為參數並返回新的 State。(應該一樣也是新的 State 直接取代掉舊的 State)
function Counter () {
const [ count, setCount ] = React.useState(0)
const increment = () => setCount((count) => count + 1)
const decrement = () => setCount((count) => count - 1)
return (
<React.Fragment>
<button onClick={decrement}>-</button>
<h1>{count}</h1>
<button onClick={increment}>+</button>
</React.Fragment>
);
}
調用 useState()
時接受 State 的初始值為參數,但是若初始值是來自於一個函數執行後的 Return Value,如果直接將 Function Invocation 當作參數傳入 useState()
會發現即便 React 只有在 Initial Render 時使用了函數的回傳值,每一次組件重新 Render,被當作參數傳入的 Function 都會被調用一次。這造成了程式運算資源的浪費。
所以 If the initial value for a piece of state is the result of an expensive calculation 應該改傳入 useState()
一個函數為參數,當此函數被調用時,會得到 The initial value of the state。差別在於,當 useState()
被傳入的是一個函數,useState()
只會在 Initial Render 調用這個函數。
**兩者差別在於傳入 useState()
的是一個 Function Invocation 還是 Function Definition。**傳入 Function definition rather than function invocation.
function getExpensiveCount () {
console.log('Calculating initial count')
return 999
}
function Counter() {
const [count, setCount] = React.useState(() => getExpensiveCount())
// Rather than getExpensiveCount()
const increment = () => setCount((count) => count + 1)
const decrement = () => setCount((count) => count - 1)
return (
<React.Fragment>
<button onClick={decrement}>-</button>
<h1>{count}</h1>
<button onClick={increment}>+</button>
</React.Fragment>
);
}
在做練習的時候觀摩到了一個能夠傳入除了 Event Object 以外的參數給事件監聽函數的方法。
<button onClick={() => removeTodo(id)}>X</button>
範例是做一個 Todo App,完整範例如下,以物件組成的陣列來儲存 Todo List 是很好的做法,還可以額外多存一個隨機產生的 id,提供取消按鈕辨認是要取消哪一個 Todo。
function generateId () {
return '_' + Math.random().toString(36).substr(2, 9);
}
function Todo () {
const [todos, setTodos] = React.useState([])
const [input, setInput] = React.useState('')
const handleSubmit = () => {
// 以物件組成的陣列作為 Todo List 的資料結構
setTodos((todos) => todos.concat({
text: input,
id: generateId()
}))
setInput('')
}
const removeTodo = (id) => setTodos((todos) => todos.filter((t) => t.id !== id))
return (
<div>
<input
type='text'
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder='New Todo'
/>
<button onClick={handleSubmit}>Submit</button>
<ul>
{todos.map(({ text, id }) => (
<li key={id}>
<span>{text}</span>
<button onClick={() => removeTodo(id)}>X</button>
</li>
))}
</ul>
</div>
);
}
Given the array of "posts", recreate the functionality for this app. By default, each post preview is cut off until the user clicks "Open". Only one post can be "Open" at a time.
我一開始的想法是額外為每一個 post 增加一個 expand
的屬性,當按鈕被點擊時,改變 post 的 expand
值。畫面會根據 expand
為 true
或是 false
來決定是否展開 post。
但在參考解答之後,發現可以不需要修改資料本身,例如為每一個 post 增加一個 expand
的屬性,這表示可以不需要修改本來的資料源頭,應該是一個更好的做法。
這個做法的核心概念是,App
本身有一個 state 叫做 openIndex
用來表示哪一則 post 是展開的,而按鈕所綁定的事件監聽函數負責調用 setOpenIndex()
並傳入被展開的 post id。
另外隱藏多餘文字所使用的 substring()
方法也是第一次接觸到,比設置 CSS 樣式規則容易。利用 CSS 隱藏多餘文字
function Post({ post, isOpen, handleClick }) {
const showText = isOpen ? post.text : post.text.substring(0, 100) + '...'
return (
<div className="post">
<img src={ post.img } className="post_img"/>
<div className="post_text">{ showText }</div>
{(isOpen) ? null : <button type="button" className='post_btn' onClick={() => { handleClick(post.id) }}>Open</button>}
</div>
)
}
function App ({ posts }) {
const [openIndex, setOpenIndex] = React.useState(null)
return (
<div className='app'>
{ posts.map((post, index) => {
const isOpen = index === openIndex
return <Post key={ index } post={ post } isOpen={ isOpen } handleClick={ setOpenIndex }/>
})}
</div>
)
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App posts={[
{
id: 0,
img: 'https://ui.dev/images/content/code-splitting.png',
text: 'Code splitting has gained popularity recently for its ability to allow you to split your app into separate bundles your users can progressively load. In this post we’ll take a look at not only what code splitting is and how to do it, but also how to implement it with React Router.'
},
{
id: 1,
img: 'https://ui.dev/images/content/composition-vs-inheritance.png',
text: 'The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong.'
},
{
id: 2,
img: 'https://ui.dev/images/content/modules.png',
text: 'I’ve taught JavaScript for a long time to a lot of people. Consistently the most commonly under-learned aspect of the language is the module system. There’s good reason for that. Modules in JavaScript have a strange and erratic history. In this post we’ll walk through that history and you’ll learn modules of the past to better understand how JavaScript modules work today.'
}
]} />,
rootElement
);
React.useEffect()
- Adding Side EffectsThe key to writing bug-free software is to maximize the predictability of your program. One strategy to do that is to minimize and encapsulate the side effects in your program.
當函數被執行時,函數外部可觀察到的變化。
In programming, a side effect is a state change that can be observed outside of its local environment. Said differently, a side effect is anything that interacts with the outside of the local function that’s being executed.
常見的 Side Effects (我認為這裡舉的例子是 Function Component 比較常 Perform 的 Side Effects)
Mutating non-local variables
Making network requests
Updating the DOM
為了提高程式碼的 Predictability 減少程式中的 bugs,Encapsulating the Side Effects 是其中一種方法。當 React Function Component 要執行 Side Effects 時,利用useEffect()
Hook to Encapsulate Side Effects。透過 useEffect()
在 Function Component 裡 Perform Side Effects。在 SIde Effects 的定義裡所說的函數,在 React 裡指的就是 Functiom Component,所以像是 Mutating Non-local Variables, Making Network Requests, and Updating the DOM 都透過 useEffect()
來執行。
關於 useEffect()
有三個重要的面向
React.useEffect()
To add a side effect to your React component, 調用 useEffect()
。useEffect()
接受一個函數 (the effect) 為參數,當作參數傳入的函數用來定義要執行的 Side Effects。
React.useEffect(() => {
document.title = 'The new title.'
})
By default, 傳入 useEffect()
的 Effect Function 會在組件每次 Render 後被重新調用。
// 按鈕觸發調用 setCount,count 被更新,組件重新 render,每次 render 後 effect 會被重新調用
// 所以 Rendering 會在 In useEffect, after render 之前先被打印出來
function Counter () {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
console.log('In useEffect, after render')
document.title = `Count: ${count}`
})
console.log('Rendering')
return (
<React.Fragment>
<button onClick={() => setCount((c) => c - 1)}>-</button>
<h1>Count: {count}</h1>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</React.Fragment>
)
}
The Effect 會在 React 更新 DOM and the browser has painted those updates to the view 之後才被調用 (也就是在 render()
被調用之後),這是為了確保 Side Effects 並不會 Block (阻擋) Updates to the UI。例如,若 Side Effect 是一個非同步任務,那麼可能會因為等待時間過長,阻擋了 UI 的更新。
但這會造成一個問題,若 The Effect 更新了 State 造成 Rerender,那麼 The Effect 就會再次被調用,形成一個 Infinite Loop。
useEffect()
接受一個 Array (Dependency Array) 作為第二個參數,這個 Array 包含所有 Side Effect 依賴的值,也就是說若 Array 裡的任何一個值在 Renders 之間產生變化,那麼 React 就會在渲染後重新調用 The Effect。更詳細一點說,The Effect 會在 Function Component 第一次 Render 後調用,之後會根據第二個參數裡的值是否有變化決定是否重新調用。
三種使用情境
No second argument
An array of all outside values the effect depends on
An empty array (assuming your effect doesn’t depend on any values)
function Profile ({ username }) {
const [profile, setProfile] = React.useState(null)
// 因為此 component 的目的是要 display profile information
// 所以為組件添加 a piece of state that represents the user’s profile
React.useEffect(() => {
getGithubProfile(username)
.then(setProfile)
// then() 直接傳入一個函數
}, [username])
// 在 username 改變時 reinvoke
if (profile === null) {
return <p>Loading...</p>
}
return (
<React.Fragment>
<h1>@{profile.login}</h1>
<img
src={profile.avatar_url}
alt={`Avatar for ${profile.login}`}
/>
<p>{profile.bio}</p>
</React.Fragment>
);
}
在 useEffect()
裡有設計一個機制,若 useEffect()
的 Effect 函數內返回一個函數 (Cleanup Function),這個被返回的函數會在兩個時機被調用
組件從 DOM 中被移除前
Invoked right before removing the component from the DOM
組件被重新 Render 後,Render 後的 Effect 被調用前,前一個 Effect 的 Cleanup Function 會被調用
If your component is re-rendered, the cleanup function for the previous render’s effect will be invoked before re-invoking the new effect.
可以利用此函數來當 Cleanup function to clean up side effects。
import { subscribe, unsubscribe } from './api'
function Profile ({ username }) {
const [profile, setProfile] = React.useState(null)
React.useEffect(() => {
subscribe(username, setProfile)
// 是在 effect 內返回
return () => {
unsubscribe(username)
setProfile(null)
}
}, [username])
if (profile === null) {
return <p>Loading...</p>
}
return (
<React.Fragment>
<h1>@{profile.login}</h1>
<img
src={profile.avatar_url}
alt={`Avatar for ${profile.login}`}
/>
<p>{profile.bio}</p>
</React.Fragment>
);
}
在這個例子裡,Cleanup Function 會在兩個情況下被調用
username
改變後,組件被重新 Render 後,新的 Effect 被調用前Cleanup Function 被調用時,根據函數內的指令,Application 會 Unsubscribe from the API 並將 profile
的值設為 null
。
useEffect
與 Lifecycle Events
將兩個概念分開來理解,不需要互相做比較,基本上 Mental Model 是不同的。兩者並無明顯的對應關係,僅能說利用 userEffect
某些情境下的特性恰好可達成以往特定生命週期的效果。
React.useReducer()
- Managing Complex StateReduce 是一個 Functional Programming Pattern (函數式編程的模式),這種模式會利用函數接受一個 Collection (包含陣列與物件) 為參數並返回一個單一的值。在 JavaScript 中,陣列的 reduce()
方法就是使用了這個模式。以下比較 Array.prototype.forEach()
與 Array.prototype.reduce()
的差異。
Array.prototype.forEach()
forEach()
是 Array 的一個方法,接受一個函數為參數與 Optional 的 thisArg
為參數。forEach()
方法會自動傳入 currentValue
, index
, array
給 Callback。每 Iterate 一個 Item 就調用 Callback 一次。The forEach()
method executes a provided function once for each array element.
arr.forEach((currentValue, index, array) => {
// execute something
}[, thisArg])
// index, array, thisArd 是 optional
currentValue
- The current element being processed in the array.index
- The index of currentValue in the array.array
- The array forEach()
was called upon.thisArg
- Value to use as this
when executing callback. 執行 Callback 的 this
值。利用 forEach()
來計算一個數字陣列的總和
const nums = [2,4,6]
let state = 0
nums.forEach((value) => {
state += value
})
可以發現 Callback 是一個 Impure Function。
Array.prototype.reduce()
reduce()
是 Array 的一個方法,接受一個稱為 Reducer 的 Callback 函數與一個 Optional 的 initialValue
為參數。reduce()
會自動傳入以下四個參數給 Reducer。每 Iterate 一個 Item 就調用 Reducer 一次,且 Reducer 會回傳一個 Return Value。The reduce()
method executes a reducer function (that you provide) on each element of the array, resulting in single output value.
accumulator
- 用來累積 Reducer 每次被調用的回傳值,最終會返回一個累加數值。若有傳入 initialValue
,initialValue
會被當作第一次調用 Reducer 時 accumulator
的初始值,且 Reducer 從 index = 0 開始調用。若沒有傳入 initialValue
則陣列的第一個元素會被當作 accumulator
的初始值,且 Reducer 從 index = 1 開始調用。currentValue
- 陣列當中的 itemcurrentIndex
- item 的 indexarray
- 調用 reduce()
的陣列所以用來計算一個陣列總和的方式可以變成
const nums = [2,4,6]
const initialState = 0
function reducer (state, value) {
return state + value
}
const total = nums.reduce(reducer, initialState)
Array.prototype.reduce()
與 Array.prototype.forEach()
的差異reduce()
能夠將每一次 Iteration 後的結果當作第一個參數傳到下一次的 Iteration,而不需要依靠函數外的變數。這使得 reduce()
是一個 Pure Function。
React.useReducer()
為什麼前面要提到 Reduce Pattern?是為了瞭解 Reduce Pattern 在程式當中的應用。React Hooks 當中的 useReducer()
也應用到了相同的 Pattern。試想一下,若調用 reduce()
方法的並不是一個陣列而是 A Collection of User Actions (使用者行為的一個集合),能不能透過這一連串的 User Actions 調用一個 Reducer 函數得到一個最終的 State?也就是說,陣列調用 reduce()
得到一個單一的值,A Collection of User Actions 調用 React.useReducer()
得到一個最終的 State。
調用 React.useReducer()
可以為 Function Component 添加 Local State 且以 Reducer 函數 (使用 Reduce Pattern) 來管理這個 State。所以與 useState()
一樣都可以為 Function Component 添加 State 但是管理 State 的方式不相同。
useReducer()
接受一個 Reducer 函數與 State 的初始值為參數並返回一個陣列。dispatch()
函數。dispatch()
修改 State,當 dispatch()
被調用時會調用 Reducer 函數修改 State,並重新渲染組件。與陣列的 reduce()
方法最終返回一個單一的值不同,因為 useReducer()
必須提供一個可以讓 User Actions 調用 Reducer 函數的方式,也就是 dispatch()
。
而且 useReducer()
與 JavaScript 的 Array.prototype.reduce()
另外一個不同的是,調用 Array.prototype.reduce()
會自動調用 Reducer Callback 然後返回一個單一的值,但 useReducer()
提供了 dispatch()
函數,當 dispatch()
被調用時才會調用 Reducer Callback。
我的理解是,陣列是已經有已知的 Item 才能夠一次 Loop Through 調用 Reducer 得到最後的返回值。組件則是要等待 User Actions 才能夠調用 Reducer (User Actions 就像是陣列裡的 Items),所以提供了 dispatch()
這個函數。
const [state, dispatch] = React.useReducer(
reducer,
initialState
)
Reducer 函數接受 state
與 value
兩個參數,state
的值會是 Previous State 且會被 React 自動傳入。任何傳入 dispatch()
的參數會被當作 value
的值傳入 Reducer。Reducer 必須要有一個 Return Value,返回值會被當作新的 state
,因為 State 改變,組件會重新渲染。
state
- Previous Statevalue
- 就是 dispatch()
的參數useReducer()
的實際應用與其將 State 的變化傳入 dispatch()
,在實際場景的應用中,更多的是將 The Type of Action 傳給 dispatch()
,reducer()
再透過傳入的 Type 決定如何更新 State。
Action 用來代表在 Application 中會改變 State 的事件。
透過這種方式,useReducer()
allows you to decouple how the state is updated from the action that triggered the update. (這句話十分重要但我還無法理解)
為了讓參數名稱更貼近這樣的應用概念,將 reducer()
的第二個參數改命名為 action
function reducer (state, action) {
if (action === 'increment') {
return state + 1
} else if (action === 'decrement') {
return state - 1
} else if (action === 'reset') {
return 0
} else {
throw new Error(`This action type isn't supported.`)
}
}
function Counter() {
const [count, dispatch] = React.useReducer(
reducer,
0
)
return (
<React.Fragment>
<h1>{count}</h1>
<button onClick={() => dispatch('increment')}>
+
</button>
<button onClick={() => dispatch('decrement')}>
-
</button>
<button onClick={() => dispatch('reset')}>
Reset
</button>
</React.Fragment>
)
}
useState()
與 useReducer()
useState()
與 useReducer()
都可以為 Function Component 添加 Local State,並提供修改 State 的方式。但兩者在使用上有什麼差異?什麼情況下使用哪一種比較恰當?
我的理解就是步驟與結果的差別。
The imperative approach is concerned with HOW you’re actually going to get a seat. You need to list out the steps to be able to show HOW you’re going to get a table. The declarative approach is more concerned with WHAT you want, a table for two.
雖然說這是兩種不同的編程方式,但通常 Declarative Programming 在更底層會包含 Imperative Programming。
Imperative vs Declarative Programming 補充閱讀
useState()
屬於 Imperative State Update, useReducer()
屬於 Declarative State Update當使用 useState()
為 Function Component 添加 Local State 的時候,是使用 Imperative Programming 的方式,以比較符合電腦運作模式的方式來撰寫程式碼,專注在執行的步驟。It’s a pretty imperative approach to solving the problem. We’re conforming to the operational model of the machine by describing how we want to accomplish the task.
當使用 useReducer()
為 Function Component 添加 Local State 的時候,是使用 Declarative Programming 的方式,以比較便於開發者思考的模式來撰寫程式碼,專注在執行的結果。
useReducer()
之所以比 useState()
更具有 Declarative Programming 的特性是因為透過 Reducer 函數將修改 State 的方式與 State 的變化對應起來,Map actions to state transitions。所以可以透過將 action
傳入 dispatch()
宣告修改 State 的方式,dispatch()
便會調用 reducer()
執行被封裝在 reducer()
內部的 Imperative, Instructional Code,而不是像使用 useState()
時一樣必需不斷調用 set...()
函數詳列每一步的 Imperative Programming。
以下以一個可以讓使用者註冊的組件為例,使用者必須輸入 username
, email
, 和 password
,而組件要能夠呈現 loading
, error
, 與 registered
。
useState()
實現function Register() {
const [username, setUsername] = React.useState('')
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState('')
const [registered, setRegistered] = React.useState(false)
const handleSubmit = (e) => {
e.preventDefault()
setLoading(true)
setError('')
newUser({username, email, password})
.then(() => {
setLoading(false)
setError('')
setRegistered(true)
}).catch((error) => {
setLoading(false)
setError(error)
})
}
if (registered === true) {
return <Redirect to='/dashboard' />
}
if (loading === true) {
return <Loading />
}
return (
<React.Fragment>
{error && <p>{error}</p>}
<form onSubmit={handleSubmit}>
<input
type='text'
placeholder='email'
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<input
type='text'
placeholder='username'
onChange={(e) => setUsername(e.target.value)}
value={username}
/>
<input
placeholder='password'
onChange={(e) => setPassword(e.target.value)}
value={password}
type='password'
/>
<button type='submit'>Submit</button>
</form>
</React.Fragment>
)
}
useReducer()
實現function registerReducer(state, action) {
if (action.type === 'login') {
return {
...state,
loading: true,
error: ''
}
} else if (action.type === 'success') {
return {
...state,
loading: false,
error: '',
registered: true
}
} else if (action.type === 'error') {
return {
...state,
loading: false,
error: action.error,
}
} else if (action.type === 'input') {
return {
...state,
[action.name]: action.value
}
} else {
throw new Error(`This action type isn't supported.`)
}
}
const initialState = {
username: '',
email: '',
password: '',
loading: false,
error: '',
registered: false
}
function Register() {
const [state, dispatch] = React.useReducer(
registerReducer,
initialState
)
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: 'login' })
newUser({
username: state.username,
email: state.email,
password: state.password
})
.then(() => dispatch({ type: 'success' }))
.catch((error) => dispatch({
type: 'error',
error
}))
}
if (state.registered === true) {
return <Redirect to='/dashboard' />
}
if (state.loading === true) {
return <Loading />
}
return (
<React.Fragment>
{state.error && <p>{state.error}</p>}
<form onSubmit={handleSubmit}>
<input
type='text'
placeholder='email'
onChange={(e) => dispatch({
type: 'input',
name: 'email',
value: e.target.value,
})}
value={state.email}
/>
<input
type='text'
placeholder='username'
onChange={(e) => dispatch({
type: 'input',
name: 'username',
value: e.target.value,
})}
value={state.username}
/>
<input
placeholder='password'
onChange={(e) => dispatch({
type: 'input',
name: 'password',
value: e.target.value,
})}
value={state.password}
type='password'
/>
<button type='submit'>Submit</button>
</form>
</React.Fragment>
)
}
因為 Reducer 函數接受 Current State 為第一個參數,所以要以一個 State (Another Piece ogf State) 來更新另一個 State (One Piece of State) 是一件簡單的事情。可以說當需要以一個 State 更新另一個 State 的時候,使用 useReducer()
為 Function Component 添加 Local State。
Whenever updating one piece of state depends on the value of another piece of state, reach for useReducer()
當在使用 useEffect()
的時候,如何正確的列出 Dependency Array 是一個很重要的事情。若沒有列出 Effect 依賴的值,Effect 可能不會被調用,若是列出太多 Effect 依賴的值,Effect 又有可能沒有在需要被調用時調用 (這句話我翻譯過來怪怪的)。
useReducer()
可以用來最小化 Dependency Array。這是因為 useReducer()
將 State 是如何被更新的與觸發更新 State 的 Action 分離開了。useReducer()
allows you to decouple how the state is updated from the action that triggered the update. 如此一來,若 Effect 調用了 dispatch()
修改 State,只是 Dispatch the type of action,而沒有依賴任何的 State Values,只需要知道 State 發生什麼改變 (What) 而不需要知道 State 如何改變 (How),所以可以將 Dependency Array 裡的值排除掉。
React.useEffect(() => {
setCount(count + 1)
}, [count])
React.useEffect(() => {
dispatch({
type: 'increment'
})
}, [])
不過我的理解是,是不是只能用在 useEffect()
要用來同步 State 的時候,畢竟用到了 useReducer()
這是用來添加與管理 State 的。
這裡用到了一個定時器的例子要回來看一下,因為跟我面試遇到的寫法不一樣。我的理解是,在 window
上設置定時器算是 Uppdating the DOM 所以需要調用 useEffect()
to encapsulate the side effect。
React.useEffect(() => {
const id = window.setInterval(() => {
setCount(count + 1)
// 我個人覺得這個例子很奇怪,如果要根據目前的 count + 1,直覺上就會傳入一個 callback 給 setCount,callback 自動被傳入當下的 count,Effect 就不用依賴 count
}, 1000)
return () => window.clearInterval(id)
}, [count])
根據上面的作法,每一次 count
改變,Effect 會被重新調用,而前一個 Effect 會清除前一個定時器。這並不是一個好的做法。更理想的作法為,一開始設好一個定時器後,定時器一直存在直到這個組件從 DOM 移除。所以要讓 Effect 不會在組件因為 count
改變重新渲染後被重新調用,這樣定時器就不會在每次渲染後被清掉 (所以是說 Effect 不會再重新渲染後備調用,前一個 Effect 的 Cleanup 函數也不會被調用嗎?)。
我的理解是這樣,修改 State 的方式分兩種
定時器現在每一秒會根據當下的 count
State 加上 1,如果可以讓為 count
加上 1 不需要依賴當下的 count
,Effect 就不需要依賴 count
。所以可以透過 useReducer()
將如何修改 State 的方式封裝在 Reducer 裡,只要讓 Reducer 知道要執行什麼修改,這樣 count
的修改就不需要依賴當下的 count
了,Effect 也就不需要依賴 count
,同一個定時器就可以一直存在直到組件從 DOM 上移除。
React.useEffect(() => {
const id = window.setInterval(() => {
dispatch({ type: 'increment' })
}, 1000)
return () => window.clearInterval(id)
}, [])
另一個作法如下,也可以抬除掉 Effect 依賴的值,所以不會在每次組件重新渲染後,調用 Effect。
React.useEffect(() => {
const id = window.setInterval(() => {
setCount((count) => count + 1)
}, 1000)
return () => window.clearInterval(id)
}, [])
React.useRef()
- Preserving Values Between RendersReact.useRef()
HookuseState()
的功能在某些情況下,需要在組件的渲染與渲染之間保留不是組件數據狀態的值 (Preserving values between renders),因為不是組件的 State,UI 並不依賴這個值,所以不需要因為這個值改變而重新渲染組件 (Re-render Component)。例如以下的情況
function Counter () {
const [count, setCount] = React.useState(0)
let id
const clear = () => {
window.clearInterval(id)
}
React.useEffect(() => {
id = window.setInterval(() => {
setCount(c => c + 1)
}, 1000)
return clear
}, [])
return (
<div>
<h1>{count}</h1>
<button onClick={clear}>Stop</button>
</div>
)
}
由於 id
是在 useEffect()
內被賦值,但 clear()
也需要存取到 id
,所以在最上層的 Scope 宣告 id
,但由於 id
的值並沒有被保留,當 count
State 改變,組件重新渲染後,id
會被重新宣告並被賦值 undefined
,因為 Effect 沒有再次被調用,id
維持 undefined
。
React.useRef()
React.useRef()
提供了在組件渲染之間保留值的功能,但不觸發組件的重新渲染。Persist a value across renders without causing a re-render.
.useRef()
接受一個初始值作為第一個參數,並返回一個物件。current
屬性,其值最初為初始值。之後任何被加到 current
屬性上的值 (操作 id.current
),都會在組件渲染之間被保留下來。所以代表 useRef()
所保留的值是可以被改變的,只是不會觸發組件的重新渲染。React.useRef()
.useRef()
實務上最常被用來取得 DOM 的節點。
ref
屬性ref
屬性接受一個 Callback 函數作為值,當元素掛載完成後,React 會調用這個函數,並將元素在 DOM 中的 Reference 傳入這個函數。所以 DOM 節點的 Reference 被當作初始值傳入 useRef()
並返回一個物件,該物件的 current
屬性便會保存 DOM 節點的 Reference。
function Form () {
const nameRef = React.useRef()
// 為什麼會有 function invocation operator ?
const emailRef = React.useRef()
const passwordRef = React.useRef()
const handleSubmit = e => {
e.preventDefault()
const name = nameRef.current.value
const email = emailRef.current.value
const password = passwordRef.current.value
console.log(name, email, password)
}
return (
<React.Fragment>
<label>
Name:
<input
placeholder="name"
type="text"
ref={nameRef}
/>
</label>
<label>
Email:
<input
placeholder="email"
type="text"
ref={emailRef}
/>
</label>
<label>
Password:
<input
placeholder="password"
type="text"
ref={passwordRef}
/>
</label>
<hr />
<button onClick={() => nameRef.current.focus()}>
Focus Name Input
</button>
<button onClick={() => emailRef.current.focus()}>
Focus Email Input
</button>
<button onClick={() => passwordRef.current.focus()}>
Focus Password Input
</button>
<hr />
<button onClick={handleSubmit}>Submit</button>
</React.Fragment>
)
}
If you want to add state to your component that persists across renders and can trigger a re-render when it’s updated, go with useState()
or useReducer()
. If you want to add state (我個人認為 UI 依賴的值才算 State) to your component that persists across renders but doesn’t trigger a re-render when it’s updated, go with useRef()
.
本來組件之間若要共享數據狀態,需要將數據狀態提升至最近的公共父組件,再由 Props 傳遞下來。但隨著應用程式逐漸變得複雜,可能會造成 Deep Props Passing 的問題,程式碼變得不易管理。React 提供了一個 Built-in API Context 解決這個問題。
React.js 的 Context 概念上就像是組件樹中某一棵子樹的全局變量,可以被子樹中的組件直接存取,不需要透過 props
一層層傳遞。一個組件的 Context 只有他的子組件能夠存取。
Typically, we create a new Context for each unique piece of data that needs to be available throughout the component tree. 基本上為每一個需要被共享的數據狀態建立一個 Context 物件。
調用 createContext()
,返回一個 Context 物件,物件內有 Provider
與 Consumer
這兩個屬性,皆為 React Component。實務上會將 createContext()
返回的 Context 物件存在一個變數裡,變數命名原則為 {state 名稱}Context
.Provider
allows us to “declare the data that we want available throughout our component tree”. 用來宣告要放到 Context 的數據
.Consumer
allows “any component in the component tree that needs that data to be able to subscribe to it”. 用來存取 Context 裡的數據,且透過
.Consumer
存取共享數據的組件會自動監聽數據變化,當共享數據發生改變時,有透過.Consumer
組件存取共享數據的組件都會被重新渲染
<Context.Provider>
有一個 value
屬性,value
的值能夠被 <Context.Provider>
的任何子組件透過 <Context.Consumer>
直接存取。所以在使用上利用 <Context.Provider>
組件將負責提供共享數據的組件包裝起來,並將本來要透過 props
往下傳遞的數據賦值給 Context.Provider
的 value
Property。<Context.Provider>
的任何子組件就能夠透過 <Context.Consumer>
直接存取 value
裡的數據。
<Context.Provider>
以下的子組件就可以透過 <Context.Consumer>
組件直接存取 value
裡的 State。<Context.Consumer>
uses a render prop (children
Props 感覺比較正確,因為是在 Body 裡放一個函數)。<Context.Consumer>
內部包裝一個函數,此函數接受 value
Property 裡的值並返回一個 JSX 物件。
React Render Prop
詳見 React Hooks - Custom Hooks
要更新 Context 裡的共享數據,做法上需要特別注意,所以在如何使用 React 的 Context API 的部份中獨立出來討論。
在沒有使用 Context 來共享數據時,會以 props
來傳遞共享數據。若是要共享修改數據的函數,也是以 props
傳遞下去。所以直覺上當把共享數據放到某一個組件的 Context 上,用來修改共享數據的函數也可以一併放到 Context 上。以物件的方式賦值給 value
。
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
locale: 'en'
}
}
render() {
return (
<LocaleContext.Provider
// value 接受一個物件為值,物件包含共享數據與修改共享數據的方法
value={{
locale: this.state.locale,
toggleLocale: : () => {
this.setState(({ locale }) => ({
locale: locale === "en" ? "es" : "en"
}));
}
}}>
<Home />
</LocaleContext.Provider>
)
}
}
這樣的做法雖然沒有錯,但是非常消耗瀏覽器資源。因為當 value
這個屬性的值 (共享數據) 發生改變時 (Data Changes),React 會自動渲染所有透過 <Consumer>
存取 value
裡的值的子組件。Whenever the data passed to value changes, React will re-render every component which used Consumer to subscribe to that data. 而在 React 裡判定數據是否發生改變的方法是透過 Reference Identity (其實就是 oldObject === newObject
)。
所以當 value
的值是一個物件時,以上述例子來看,每一次 <App/>
組件重新渲染時,都會傳遞一個新的物件的 Reference 給 value
導致所有共享數據的子組件都會重新渲染一遍。
為了避免渲染時的資源浪費,將同一個一個包含共享數據與修改共享數據函數的物件的 Reference 賦值給 value
,而不是每次賦值一個新的物件。換句話說傳入同一個物件的 Reference 而不是每次傳入一個新的物件的 Reference。以下提供兩種解決辦法。
this.state
物件class App extends React.Component {
constructor(props) {
super(props)
this.state = {
locale: 'en',
toggleLocale: () => {
this.setState(({ locale }) => ({
locale: locale === "en" ? "es" : "en"
}));
}
}
}
render() {
return (
<LocaleContext.Provider value={this.state}>
<Home />
</LocaleContext.Provider>
)
}
}
如此一來,<App>/
以下的子組件仍然可以透過 Context 存取共享數據與修改數據的方法,且不浪費渲染資源。
import React from 'react'
import LocaleContext from './LocaleContext'
export default function Blog () {
return (
<LocaleContext.Consumer>
{({ locale, toggleLocale }) => (
<React.Fragment>
<Nav toggleLocal={toggleLocale} />
<Posts locale={locale} />
</React.Fragment>
)}
</LocaleContext.Consumer>
)
}
React.useMemo()
React.useMemo()
React.useMemo()
接受兩個參數,一個函數與一個陣列,陣列裡包含了函數依賴的值。React.useMemo()
返回一個值 (我的理解是這個值就是被當作參數傳入的函數的 Return Value)。這個值只會在依賴的值改變時才會更新。我的理解是透過 React.useMemo()
,Callback 只會在依賴的值改變時才重新被調用並返回新的值。
const memoizedValue = useMemo(() =>
computeExpensiveValue(a, b),
[a, b]
)
程式碼可以改寫成。This way, any component which used Consumer to subscribe to our locale context will only re-render if locale changes.
export default function App () {
const [locale, setLocale] = React.useState('en')
const toggleLocale = () => {
setLocale((locale) => {
return locale === 'en' ? 'es' : 'en'
})
}
const value = React.useMemo(() => ({
locale,
toggleLocale
}), [locale])
// 當 locale 改變才需要重新渲染所有有使用 locale 的子組件
return (
<LocaleContext.Provider value={value}>
<Home />
</LocaleContext.Provider>
)
}
...
import React from 'react'
import LocaleContext from './LocaleContext'
export default function Blog () {
return (
<LocaleContext.Consumer>
{({ locale, toggleLocale }) => (
<React.Fragment>
<Nav toggleLocal={toggleLocale} />
<Posts locale={locale} />
</React.Fragment>
)}
</LocaleContext.Consumer>
)
}
有一個共享數據叫 locale
,調用 React.createContext()
建立一個叫做 LocaleContext
的物件,透過 <LocaleContext.Provider>
的 value
屬性將共享數據放到被包裝組件的 Context 上,子組件透過 <LocaleContext.Consumer>
存取共享數據與監聽數據變化。
基本上為每一個需要被共享的數據狀態建立一個 Context 物件,這個 Context 物件會賦值給與共享數據同名的變數。
defaultValue
當一個 <Consumer/>
組件被渲染的時候,會往上層找,從最近的且是來自相同的 Context 物件的 <Provider>
組件存取 value
屬性的值。
但如果往上找不到相同 Context 物件的 <Provider>
組件呢?<Consumer/>
組件會以 createContext()
的第一個參數來當作本來要從 <Provider/>
組件取得的 value
。所以 createContext()
的第一個參數就是 <Consumer/>
組件的 Default Value。
const MyContext = React.creatContext('defaultValue')
Here’s the thing, when you’re a hammer, everything looks like a nail. Typically when you first learn about Context, it appears like it’s the solution to all your problems. Just remember, there’s nothing wrong with passing props down multiple levels, that’s literally how React was designed. I don’t have a universal rule for when you should and shouldn’t use Context, just be mindful that it’s common to overuse it.
React.useContext()
在 Context API 裡 <Context.Consumer>
內部包裝一個函數,此函數接受 <Context.Provider>
value
Property 裡的值並返回一個 JSX 物件。透過這個方式存取到 Context 裡的共享數據與監聽數據變化。但是當有多個 Context Value 要存取的時候,程式碼便會顯得有點複雜。
export default function Nav () {
return (
<AuthedContext.Consumer>
{({ authed }) => authed === false
? <Redirect to='/login' />
: <LocaleContext.Consumer>
{({ locale, toggleLocale }) => locale === "en"
? <EnglishNav toggleLocale={toggleLocale} />
: <SpanishNav toggleLocale={toggleLocale} />}
</LocaleContext.Consumer>}
</AuthedContext.Consumer>
)
}
React Hooks API 的 React.useContext()
提供了一個更 Composable 的作法。
React.useContext()
接受一個 Context 物件為第一個參數並返回來自相同 Context 且是最近的 Provider 組件裡 value
的值。換句話說 React.useContext()
與 <Context.Consumer>
功能一樣,但更 Composable。所以上面的例子就可以改寫成如下
// 我個人的理解是 Context 物件要被引入這個組件的 file 用來調用 React.useContext()`
export default function Nav () {
const { authed } = React.useContext(AuthedContext)
if (authed === false) {
return <Redirect to='/login' />
}
const { locale, toggleLocale } = React.useContext(
LocaleContext
)
return locale === 'en'
? <EnglishNav toggleLocale={toggleLocale} />
: <SpanishNav toggleLocale={toggleLocale} />
}
React.memo()
, React.useCallback()
, and React.useMemo()
- PerformanceReact 的核心概念,UI is just a function of state。
UI = Fn(State)
所以在 React 裡定義 Component 的時候,基本上就是定義 JavaScript 函數。JavaScript 函數接受參數並返回值。Function takes in some arguments and returns a value。React 組件接受 Props 為參數並返回一個 JSX (描述 UI 的物件)。Component takes in some arguments (props) and returns an object representation of your UI (JSX). 當組件的 State 改變或被傳入新的 Props,會重新渲染組件。
在 React Hooks API 出現之前可能 UI is just a function of state 的概念還不能算完全正確,因為只有 Class Component 能夠有 Local State 與 Custom Logic。React Hooks API 讓組件就是函數的概念變得更加完整。而當組件被重新渲染時,就像一個函數被調用時一樣,組件內的所有程式碼都會被執行。但有的時候不是所有程式碼都必須在重新渲染時被執行 (A way to opt out (選擇退出) re-rendering expensive components.)。
在 React 裡有不同的方式可以實現當 Props 沒有改變時不重新渲染組件 (或可以說是不執行一部分的程式碼) 以提升 Performance。以下以一個例子來說明不同的方式如何實現。
建立一個 App 讓使用者可以找到第 n 個費氏數列的值與第 n 個質數。<NthFib/>
和 <NthPrime/>
接受 count
與增加 count
的函數並返回一個顯示結果的 UI。
這個例子的問題在於,在 <App/>
中當 State 當中的任何一個 fibCount
或 primeCount
改變時,整個 <App/>
就會被重新渲染,其子組件 <NthFib/>
和 <NthPrime/>
也會被重新渲染,即便兩個子組件接受的 Props 可能沒有改變。若 <NthFib/>
和 <NthPrime/>
are computationally expensive 那麼不需要的渲染就會降低效能。
import React from 'react'
import { calculateFib, suffixOf } from './math'
function NthFib({ count, increment }) {
const fib = calculateFib(count)
return (
<div className='container'>
<h2>Nth Fib</h2>
<p>
The <b>{suffixOf(count)}</b> number
in the fibonacci sequence is <b>{fib}</b>.
</p>
<button onClick={increment}>Next number</button>
</div>
)
}
export default NthFib
import React from 'react'
import { calculatePrime, suffixOf } from './math'
function NthPrime({ count, increment }) {
const prime = calculatePrime(count)
return (
<div className='container'>
<h2>Nth Prime</h2>
<p>
The <b>{suffixOf(count)}</b> prime
number is <b>{prime}</b>.
</p>
<button onClick={increment}>Next prime</button>
</div>
)
}
export default NthPrime
import React from 'react'
function App() {
const [fibCount, setFibCount] = React.useState(1)
const [primeCount, setPrimeCount] = React.useState(1)
const handleReset = () => {
setFibCount(1)
setPrimeCount(1)
}
const add10 = () => {
setFibCount((c) => c + 10)
setPrimeCount((c) => c + 10)
}
return (
<React.Fragment>
<button onClick={add10}>Add 10</button>
<button onClick={handleReset}>Reset</button>
<hr />
<NthFib
count={fibCount}
increment={() => setFibCount((c) => c + 1)}
/>
<hr />
<NthPrime
count={primeCount}
increment={() => setPrimeCount((c) => c + 1)}
/>
</React.Fragment>
);
}
React.memo()
React.memo()
是一個 Higher-order Component,用來 Skip Re-rendering a Component 如果 Component 的 Props 沒有改變。
利用 React.memo()
將組件包裝起來,React 會在第一次渲染後 Memoize the Result。在後續的 Re-render 時,React 會 Shallow Compare 舊的 Props 與新的 Props (第一層的 Keys 名稱、數目、值有沒有都一樣)。若 Props 並未改變則 React 不會重新渲染組件。
在實務上將 React.memo()
引入想要被 Memoized 的組件檔案裡,然後 Export React.memo()
返回的被包裝組件
export default React.memo(wrappedComponent)
如此一來可以改寫 <NthFib/>
和 <NthPrime/>
export default React.memo(NthFib)
export default React.memo(NthPrime)
Shallow Compare
Iterate 兩個被比較物件第一層的 Keys,並比較每一個 Keys 的值。
- 採用嚴格相等 - 對於基本型別的資料,值相等算相等。對於物件型別的資料,指向同一個 Reference 算相等
- 只比較第一層且屬性名稱、數目必須一樣 - 所以叫做 Shallow。
我的理解是所以總結來說 Shallow Compare 兩個物件就是比較兩個物件第一層的 Keys 名稱、數目、值有沒有都一樣。
一個 JavaScript 的變數可以儲存兩種資料型別,也可以說 JavaScript 有兩種資料型別 (Data Types)
但即便調用了 React.memo()
會發現 <NthFib/>
和 <NthPrime/>
依舊在 <App/>
每次被渲染時重新被渲染,即便 Props 沒有產生變化。這是因為 React.memo()
是 Shallow Compare 舊的 Props 與新的 Props。當使用 Inline Function 的方式將函數當作 Props 傳給組件時,即便函數的定義相同,每一次組件渲染時都是傳遞一個新的函數 Reference 給組件。
<NthFib
count={fibCount}
increment={() => setFibCount((c) => c + 1)}
/>
<NthPrime
count={primeCount}
increment={() => setPrimeCount((c) => c + 1)}
/>
Inline Function
In JavaScript, inline function is a special type of anonymous function which is assigned to a variable. Thus anonymous function and inline function is practically the same. Unlike normal function, they are created at runtime.
React.memo()
有兩種解決辦法
React.memo()
只比較 Props 裡的一部分React.memo()
只比較 Props 裡的一部分React.memo()
接受一個函數為第二個參數,這個 Callback 函數接受兩個參數 prevProps
(舊的 Props) nextProps
(新的 Props),在 Callback 內驗證一部分的 Props 是否相等。若 Callback 返回 true
則組件不重新渲染,若返回 false
則組件重新渲染。
export default React.memo(NthFib, (prevProps, nextProps) => {
return prevProps.count === nextProps.count
})
export default React.memo(NthPrime, (prevProps, nextProps) => {
return prevProps.count === nextProps.count
})
React.useCallback()
React.useCallback()
接受兩個參數,一個函數與一個陣列,陣列裡包含了函數依賴的值。React.useCallback()
返回一個 Memoized Callback。Memoized Callback 只會在依賴的值改變時才會更新。我的理解是 React.useCallback()
返回一個 Memoized Callback 也是返回這個 Memoized Callback 的 Reference,而只有在依賴的值改變的時候,會更新這個 Reference。
const memoizedCallback = useCallback(() =>
doSomething(a, b),
[a, b],
)
所以 Instead of passing an inline function as the increment prop and creating a brand new function on every render, we can utilize useCallback to create one function on the initial render, and reuse it on subsequent renders.
const incrementFib = React.useCallback(() =>
setFibCount((c) => c + 1),
[]
)
const incrementPrime = React.useCallback(() =>
setPrimeCount((c) => c + 1),
[]
)
<NthFib
count={fibCount}
increment={incrementFib}
/>
...
<NthPrime
count={primeCount}
increment={incrementPrime}
/>
所以 React.memo()
是 React 裡實現當 Props 沒有改變時不重新渲染組件以提升 Performance 的方法。React.memo()
是 Memoizing at the Component Level。
不過以另一個角度來看,之所以要 Memoize <NthFib/>
和 <NthPrime/>
是因為兩個組件內部的 calculateFib
與 calculatePrime
函數 are computationally expensive。另一個在 React 裡實現當 Props 沒有改變時不重新渲染組件以提升 Performance 的方法是當 Computationally Expensive 的函數所依賴的 Props 有改變時,才調用 Computationally Expensive 的函數。
React.useMemo()
React.useMemo()
接受兩個參數,一個函數與一個陣列,陣列裡包含了函數依賴的值。React.useMemo()
返回一個值 (我的理解是這個值就是被當作參數傳入的函數的 Return Value)。這個值只會在依賴的值改變時才會更新。我的理解是透過 React.useMemo()
,Callback 只會在依賴的值改變時才重新被調用並返回新的值。
const memoizedValue = useMemo(() =>
computeExpensiveValue(a, b),
[a, b]
)
以此來取代 React.memo()
和 useCallback()
的做法
import React from 'react'
import { calculateFib, suffixOf } from './math'
export default function NthFib({ count, increment }) {
const fib = React.useMemo(() => calculateFib(count), [count])
return (
<div className='container'>
<h2>Nth Fib</h2>
<p>
The <b>{suffixOf(count)}</b> number
in the fibonacci sequence is <b>{fib}</b>.
</p>
<button onClick={increment}>Next number</button>
</div>
)
}
import React from 'react'
function App() {
const [fibCount, setFibCount] = React.useState(1)
const [primeCount, setPrimeCount] = React.useState(1)
const handleReset = () => {
setFibCount(1)
setPrimeCount(1)
}
const add10 = () => {
setFibCount((c) => c + 10)
setPrimeCount((c) => c + 10)
}
return (
<React.Fragment>
<button onClick={add10}>Add 10</button>
<button onClick={handleReset}>Reset</button>
<hr />
<NthFib
count={fibCount}
increment={() => setFibCount((c) => c + 1)}
/>
<hr />
<NthPrime
count={primeCount}
increment={() => setPrimeCount((c) => c + 1)}
/>
</React.Fragment>
);
}
It may seem like you can use useMemo
to persist values across renders. However, React treats useMemo
as a performance hint rather than a guarantee. This means that React may choose to forget previously memoized values under certain use cases. To persist a value across renders, use useRef
.
As with any talk about performance, I feel obligated to include that performance optimizations are never free. If they were, they’d be included by default. The same applies to React.memo
, useCallback
, and useMemo
. The default behavior of React isn’t to memoize components, functions or values because the majority of the time it’s unnecessary.