最近負責的專案遇到一點寫法上的瓶頸,codebase搞得亂七八糟的(太菜😅),所以開始看一些跟模式(pattern)有點相關的文章。
這篇筆記主要內容出自於 React component as prop: the right way™️,在講述如何傳遞元件(component)當作props。
會想傳遞components當作props,主要是為了方便分享state或props、並且讓程式碼更靈活更彈性,而文章作者將傳遞方式大致上分為三種模式:
- Pass components as Elements;
- Pass components as Components;
- Pass components as Functions
因為有在用React-Router-DOM,最熟悉的應該是第一種pattern,不過這三種傳遞方式各有優缺點,需要好好認識一下不同情境下、不同傳遞模式的使用方式。
1. Pass Component as an Element
第一種就是像下面React-Router-DOM傳遞 <Root />
和 <ErrorPage />
的方式:
這種傳遞元件方式的好處在於,可避免在不同元件之間傳遞過多props,使得程式碼變得更複雜。
以上圖React-Router-DOM的 <ErrorPage />
為例,假設每個路徑有各自的錯誤頁,如果是把元件包在路徑的父元件 <Root />
:
// Root.tsx
export function Root({hasError, errorStatus, errorMessage, errorPageStyles}){
if(hasError){
return <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} {...errorPageStyles}/>
}
...
}
// App.tsx
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles}/>
}
若是改成直接將 <ErrorPage />
當作props傳遞,可省去透過 <Root />
多傳遞一層的麻煩:
// Root.tsx
export function Root({errorPage}: {errorPage: ReactElement<ErrorPropss>}){
if(hasError){
return <>{errorPage}</>
}
...
}
// App.tsx
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} errorPage={<ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} {...errorPageStyles}/>}
}
不過這種方式的缺點在於如果要加入預設值會比較麻煩,需要用到 React.cloneElement()
:
// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';
export function Root({errorPage}: {errorPage: ReactElemeny<ErrorPropss>}){
const clonedErrorPage = React.cloneElement(errorPage, {
errorMessage: errorPage.props.errorMessage || defaultErrorMsg
})
if(hasError){
return <>{errorPage}</>
}
...
}
2. Pass Component as a Component
第二種傳遞方式的名稱看起來有點繞口,直接來看基本的程式碼:
// Root.tsx
export function Root({ErrorPage}: {errorPage: ComponentType<ErrorPropss>}){
if(hasError){
return <ErrorPage />
}
...
}
// App.tsx
const PassedErrorPage = () => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />;
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} ErrorPage={PassedErrorPage} />
}
上面的 PassedErrorPage
並沒有傳遞任何props,如果想要加入props讓這段程式碼更靈活可以寫成:
const PassedErrorPage =
(props) => <ErrorPage errorStatus={props.errorStatus} errorMessage={props.errorMessage} errorPageStyles={props.errorPageStyles} />;
或者簡單寫成:
const PassedErrorPage = (props) => <ErrorPage {...props} />;
第二種傳遞方式雖然第一眼看起來跟名稱一樣難懂,但在加入預設值方面變得方便許多:
// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';
export function Root({ErrorPage}: {errorPage: ComponentType<ErrorPropss>}){
if(hasError){
return <ErrorPage errorMessage={defaultErrorMsg}/>
}
...
}
// App.tsx
const PassedErrorPage = (props) => <ErrorPage errorStatus={props.errorStatus} errorPageStyles={props.errorPageStyles} />;
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} ErrorPage={PassedErrorPage} />
}
如果像上面範例一樣沒加入 errorMessage
props就會以預設值呈現,反之有加入就會覆蓋掉預設值。
3. Pass Component as a Function
第三種方式光看名稱也是讓人困惑😅,一樣直接看程式碼:
// Root.tsx
export function Root({renderErrorPage}: {renderErrorPage: () => ReactElement<ErrorPropss>}){
const errorPage = renderErrorPage();
if(hasError){
return <>{errorPage}</>
}
...
}
// App.tsx
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} renderErrorPage={() => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />} />
}
如果要加入預設值:
// Root.tsx
const defaultErrorMsg = 'Oops, There is something wrong!';
export function Root({renderErrorPage}: {renderErrorPage: () => ReactElement<ErrorPropss>}){
const errorPage = renderErrorPage({
errorMessage: defaultErrorMsg,
});
if(hasError){
return <>{errorPage}</>
}
...
}
// App.tsx
const passedErrorPage = () => <ErrorPage errorStatus={errorStatus} errorMessage={errorMessage} errorPageStyles={errorPageStyles} />;
export function App(){
const [hasError, setHasError] = useState(false);
return <Root hasError={hasError} renderErrorPage={passedErrorPage} />
}
小結
雖然作者最後有簡單分享她認為的三種傳遞方式使用時機,但看完還是不太了解第二、三種傳遞方式使用時機的差異😅。
不過個人見解覺得從以上程式碼可以發現,第一種傳遞方式比較能簡潔有力地表達出要傳遞的是元件;但如果要讓元件更有彈性地變化,例如客製化元件,很顯然地第二、三種傳遞方式能讓程式碼更靈活。
而相較於第一、二種傳遞方式,語法上第三種以函式傳遞元件的方式若是命名不好,會比較難一眼看出這是在傳遞元件當作props,不過在設定預設值方面的程式碼可閱讀性會比其他兩種高一點。