在前面系列的文章,已經幫大家複習並深入了解幾個常用的 hook,接下來的章節,是個人開發經驗與結合前端 design pattern 的概念的經驗分享,所以你如果 hook 還不會用的請參考我之前的文章,那麼我們就開始今天的主題吧!
今天要講的是很常見也很入門就會遇到的問題,想像一下,當你的功能越變越大的時候,你會很頻繁地和後端的 server 透過 rest api 溝通索取需要陳列的資料,所以當你要處理的 api 增加之後,你所需要陳列的畫面也就會變得很複雜。
那麼常見新手犯的錯誤就是你的 component 太肥大了,下面我用一個常見的新手範例來做示範:
import React, { useEffect, useState } from 'react'
export interface PokemonItem {
name: string;
url: string;
}
export interface PokesState {
count: number;
next: string | null;
previous: string | null;
results: PokemonItem[]
}
const Foo = () => {
const [rsData, setRsData] = useState<PokesState>({
count: 0,
next: null,
previous: null,
results: []
});
useEffect(() => {
if(!rsData.results) {
const controller = new AbortController();
fetch(`https://pokeapi.co/api/v2/pokemon`, {
signal: controller.signal
})
.then((response) => response.json())
.then((data) => {
setRsData(data)
});
return () => controller.abort();
}
}, [])
const changeList = (url: string|null) => {
if(!url) return;
fetch(url)
.then((res) => res.json())
.then((data) => setRsData(data))
}
// 因為不想處理props或覺得麻煩而放在內層,但這樣是不對的
const BtnGroup = () => {
return (
<div className='f-c-c'>
<button
className="btn filled"
disabled={rsData.previous===null}
onClick={() => changeList(rsData.previous)}
>prev-page</button>
<button
className="btn filled"
disabled={rsData.next===null}
onClick={() => changeList(rsData.next)}
>next-page</button>
</div>
)
}
return (
<div>
<BtnGroup/>
{rsData?.results?.map((e) => (
<div key={e.url}>{e.name}</div>
))}
</div>
)
}
export default Foo;
不論是哪種理由,盡量避免不必要的套件鑲嵌,也就是在 component 裏面 return 之前,令列其他子組件,而且沒有處理 props 的問題直接渲染,這樣不但會造成維護上的困擾,也會產生組件渲染上重複渲染的問題。
所以,以上述為例原本的 BtnGroup 應該拆到 Foo 組件之外,透過 props 的機制,傳入對應的 data,如下:
import React, { useEffect, useState } from 'react'
export interface PokemonItem {
name: string;
url: string;
}
export interface PokesState {
count: number;
next: string | null;
previous: string | null;
results: PokemonItem[]
}
// 這裡就是拆分的基本方式,對新手來說要定義這些type問題,是相對有門檻的
// 但站在維運角度來說,是有其必要性的
// 在通常來說,會再分離檔案出來,使其保有彈性可以擴充利用
const BtnGroup = ({prev, next, clickFn}: {prev: string|null; next: string|null; clickFn: (url: string|null) => void}) => {
return (
<div className='f-c-c'>
<button
className="btn filled"
disabled={prev===null}
onClick={() => clickFn(prev)}
>prev-page</button>
<button
className="btn filled"
disabled={next===null}
onClick={() => clickFn(next)}
>next-page</button>
</div>
)
}
const Foo = () => {
const [rsData, setRsData] = useState<PokesState>({
count: 0,
next: null,
previous: null,
results: []
});
useEffect(() => {
if(!rsData.results) {
const controller = new AbortController();
fetch(`https://pokeapi.co/api/v2/pokemon`, {
signal: controller.signal
})
.then((response) => response.json())
.then((data) => {
setRsData(data)
});
return () => controller.abort();
}
}, [])
const changeList = (url: string|null) => {
if(!url) return;
fetch(url)
.then((res) => res.json())
.then((data) => setRsData(data))
}
return (
<div>
<BtnGroup
prev={rsData.previous}
next={rsData.next}
clickFn={changeList}
/>
{rsData?.results?.map((e) => (
<div key={e.url}>{e.name}</div>
))}
</div>
)
}
export default Foo;
有了這樣的概念之後,在後續開發設計架構上,就可以將 component 大致上分為兩類:
這樣一來當你的專案出現 bug 的時候,你就能很快地去找到相對問題的組件(component) 進行修正,在初期架構階段也較能有效管理對應功能組件。
這樣的概念就是對應上 Design Pattern 的 Container/Presentational Pattern有興趣的話可以參考這個連結。
那麼我們試看看做得更徹底一點,將原本的列表封裝成組件吧!
import React, { memo, useEffect, useState } from 'react'
export interface PokemonItem {
name: string;
url: string;
}
export interface PokesState {
count: number;
next: string | null;
previous: string | null;
results: PokemonItem[]
}
const BtnGroup = ({prev, next, clickFn}: {prev: string|null; next: string|null; clickFn: (url: string|null) => void}) => {
return (
<div className='f-c-c'>
<button
className="btn filled"
disabled={prev===null}
onClick={() => clickFn(prev)}
>prev-page</button>
<button
className="btn filled"
disabled={next===null}
onClick={() => clickFn(next)}
>next-page</button>
</div>
)
}
// 這裡因為複雜度不夠的關係,做得比較陽春一點
const PokeElement = ({ poke }:{poke: PokemonItem}) => {
return(
<p>
{poke.name}
<a href={poke.url}>link</a>
</p>
)
}
// 這裡就回歸到之前講memo的優化部分,有興趣的朋友可以回顧參考
// https://medium.com/@LeeLuciano/react-%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B-memo-1cf1cca55167
const MemoPokeElement = memo(PokeElement)
const Foo = () => {
const [rsData, setRsData] = useState<PokesState>({
count: 0,
next: null,
previous: null,
results: []
});
useEffect(() => {
if(!rsData.results) {
const controller = new AbortController();
fetch(`https://pokeapi.co/api/v2/pokemon`, {
signal: controller.signal
})
.then((response) => response.json())
.then((data) => {
setRsData(data)
});
return () => controller.abort();
}
}, [])
const changeList = (url: string|null) => {
if(!url) return;
fetch(url)
.then((res) => res.json())
.then((data) => setRsData(data))
}
return (
<div>
<BtnGroup
prev={rsData.previous}
next={rsData.next}
clickFn={changeList}
/>
{rsData?.results?.map((e) => (
<React.Fragment key={e.url}>
<MemoPokeElement poke={e}/>
</React.Fragment>
))}
</div>
)
}
export default Foo
只要掌握以上原則,你就會慢慢瞭解到什麼樣的時機應該要整理你的組件(component),在 Code Review 的時候就可以順著這個概念去優化。
走出新手村需要的是思辨的能力,習慣是需要慢慢養成的,在團隊中也是一樣,團隊開發必須大家一起養成習慣,如果要別人也和你一樣養相同的習慣,團隊成員必須要有共同的概念價值,也希望大家可以真正運用在現有的專案當中。