[譯] React 路由和 React 元件的愛恨情仇

LeviDing發表於2018-12-25

作為 React 開發者,我們大部分人享用著使用 React Router 為 React 應用的路由帶來的便利。

為什麼我們 ❤️ React 路由:

  • 與 React 完美結合並且遵循相同的原則
  • 路由的導航方面非常容易理解
  • 元件組合、宣告性 UI、狀態管理 並且它緊密地追隨著 React 的工作流 (事件 => 狀態變化 => 重新渲染)
  • 可靠的 瀏覽歷史特徵 允許使用者在追蹤檢視狀態的同時在應用中導航。

然而在使用 React 路由的時候,如果你的應用程式特定需求變得比你在 web 上的每個教程中看到的常規用法稍微複雜一些,你將面對一些困難。

好訊息是即使在那些場景下,React 路由仍然允許我們以一種乾淨的方式解決問題;但是解決方案可能並不像一眼能開出來那麼明顯。這兒有個我們在 Fjong 開發團隊 ? 的案例,我們在路由路徑改變查詢引數並且期望一個元件被重新渲染,React Router 的表現卻不是那麼回事兒。

在我們描述具體問題和我們如何解決這個問題之前,讓我們聊聊 React 路由和 React 元件之間巨大關係的幾個方面。

相愛關係

React 路由和 React 元件之間有很多的聯絡。這主要是因為它們都遵循上面提到的相同的事件迴圈 (事件 => 狀態變化 => 重新渲染)。現在記住這個流程,我們將解決在應用程式中導航的一個常見問題;當路由更改的時候滾動到頁面的頂部

假設你有一組名為 HomeAboutSearch 的元件

<Router history={History}>
  <Switch>
    <Route exact path="/" component={Home}/>
    <Route exact path="/about" component={About}/>
    <Route exact path="/search" component={Search}/>
    <Route exact component={NoMatch}/>
  </Switch>
</Router>
複製程式碼

現在假設當你跳轉至 /search 的時候,你需要滾動很多次才能在 Search 頁面看到你想看到的專案。

然後,你在位址列輸入跳轉至 /about 的連結,然後突然看到了 About Us 頁面的底部,而不是頂部,這可能很煩人。這有一些方法解決這個問題,但是 React 路由為你提供了所有必要的工具來正確地完成這個任務。讓我們來看看實際情況。

/* globals window */

/* Global Dependencies */
const React = require('react');
const { Component } = require('react');
const PropTypes = require('prop-types');
const { Route, withRouter } = require('react-router-dom');

class ScrollToTopRoute extends Component {

	componentDidUpdate(prevProps) {
		if (this.props.location.pathname !== prevProps.location.pathname) {
			window.scrollTo(0, 0);
		}
	}

	render() {
		const { component: Component, ...rest } = this.props;
    
		return <Route {...rest} render={props => (<Component {...props} />)} />;
	}
}

ScrollToTopRoute.propTypes = {
	path: PropTypes.string,
	location: PropTypes.shape({
		pathname: PropTypes.string,
	}),
	component: PropTypes.instanceOf(Component),
};

module.exports = withRouter(ScrollToTopRoute);

// Usage in App.jsx
<Router history={History}>
  <Switch>
    <ScrollToTopRoute exact path="/" component={Home}/>
    <ScrollToTopRoute exact path="/about" component={About}/>
    <ScrollToTopRoute exact path="/search" component={Search}/>
    <ScrollToTopRoute exact component={NoMatch}/>
  </Switch>
</Router>
複製程式碼

討厭的關係

但是對於任何關係來說,事情並不是在每種情況下都進展順利。這與 React 路由和 React 元件的情況相同。為了更好地理解這一點,我們來看看應用程式中的一個可能的場景。

假設你要從 /search/about,然後當你到達 About Us 頁面時,頁面顯然會像你所期望的那樣重新渲染。從 /about 導航到 /search 也是如此。

現在假設從 /search?tags=Dresses/search?tags=Bags 的時候,你的 SearchPage 將搜尋查詢引數附加到 URL 上,並且你希望重新渲染這些引數。在這,我們更改了 React 路由路徑 location.path = /search 上的搜尋查詢,它被 React 路由識別為同一位置物件上的屬性 location.search = ?tags=Dresses or ?tags=Bags

無論是 React 路由還是你的元件都沒有意識到它們需要重新渲染頁面,因為從技術上講,我們還是在同一個頁面。React 元件不允許在相同路徑但是不同搜尋查詢間的路由跳轉觸發重新渲染。

目前我們的路由和元件似乎有點脫節。好難過 :(

所以,我們如何才能解決這個問題呢?其實他們每個人都有解決這個問題的方法。React 路由告訴我們 URL 中的搜尋查詢引數是否發生了變化而且更重要的是根據 React 正確的生命週期來做這件事。之後,元件將負責決定如何處理這些資訊。

在這個案例中,如果元件需要重新渲染(由一個叫 RouteKey 的 boolean 屬性(prop)決定)它將向元件傳遞一個唯一的鍵,該鍵是 location.pathnamelocation.search 的組合(這傳遞了鍵的一般經驗法則,鍵應該是唯一的、穩定的和可預測的)在這個場景中,每當路由被請求,元件都能接受一個新的鍵;而且即使你停留在同一個頁面,它也會為你重新渲染,沒有任何副作用。我們來看看它是如何在實際中放回作用的!

/* globals window */

/** Global Dependencies */
const React = require('react');
const { Component } = require('react');
const PropTypes = require('prop-types');
const { Route, withRouter } = require('react-router-dom');

class ScrollToTopRoute extends Component {

	componentDidUpdate(prevProps) {
		if (this.props.location.pathname !== prevProps.location.pathname) {
			window.scrollTo(0, 0);
		}
	}

	render() {
		const { component: Component, RouteKey, location, ...rest } = this.props;

		/**
		 * Sometimes we need to force a React Route to re-render when the
		 * search params (query) in the url changes. React Router does not
		 * do this automatically if you are on the same page when the query
		 * changes. By passing the `RouteKey`ro the `ScrollToTopRoute` and
		 * setting it to true, we are passing the combination of pathname and
		 * search params as a unique key to the component and that is a enough
		 * and clear trigger for the component to re-render without side effects
		 */
		const Key = RouteKey ? location.pathname + location.search : null;

		return <Route {...rest} render={props => (<Component {...props} key={Key} />)} />;
	}
}

ScrollToTopRoute.propTypes = {
	path: PropTypes.string,
	location: PropTypes.shape({
		pathname: PropTypes.string,
	}),
	component: PropTypes.instanceOf(Component),
	RouteKey: PropTypes.boolean,
};

module.exports = withRouter(ScrollToTopRoute);

// Usage in App.jsx
<Router history={History}>
  <Switch>
    <ScrollToTopRoute exact path="/" component={Home}/>
    <ScrollToTopRoute exact path="/about" component={About}/>
    <ScrollToTopRoute exact path="/search" component={Search} RouteKey={true} />
    <ScrollToTopRoute exact component={NoMatch}/>
  </Switch>
</Router>
複製程式碼

結論

我們介紹了React 路由和元件完美結合的例子,以及它們稍微分離時的場景。但是重要的是要記住,在大部分情況下,React 路由遵循和 React 相同的原則和設計模式,花時間熟悉這些原則及其相關的執行上下文,對於在 React 路由中修復 bug 會有很大幫助。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章