2021鐵人賽
React
還記得這個網站有篩選圖表的功能嗎?當初畫wireframe的時候考量到未來圖表可能會越來越多,因此規劃了下拉式選單來做篩選功能,如下圖:
本篇就來用React寫寫看這個篩選功能吧!
先來看看"美國製造業電子零件訂單"這個series,在FRED網站上是怎麼分類的,如下圖:
可以看到下面有一些比較詳細的資料,這邊把它放大一下,裡面包含幾項內容:
這邊稍微解釋一下series、source、release三者的關係,series是指時間序列資料,例如"美國製造業電子零件訂單"這個series是由過去每個月的數據組成,將這些數據依照時間排序就是一個序列,而一個series來自一個release,一個source通常有多個releases,一個release可能來自多個sources。
當要做篩選功能的時候,可以做的篩選方式可能就會有三種方式:
因此首先下拉式選單內要有可以選擇的source與release,這些資料可以從FRED API取得,不過如果把API可以拿到的所有sources與releases放進來的話有點太多,因此我先依照目前有用到的series去列出有用到的sources與releases就好,製作成sources.json與releases.json如下:
src\data\sources.json
[
{
"id": 1,
"realtime_start": "2021-08-22",
"realtime_end": "2021-08-22",
"name": "Board of Governors of the Federal Reserve System (US)",
"link": "http://www.federalreserve.gov/"
},
{
"id": 19,
"realtime_start": "2021-08-22",
"realtime_end": "2021-08-22",
"name": "U.S. Census Bureau",
"link": "http://www.census.gov/"
}
]
src\data\releases.json
[
{
"id": 18,
"realtime_start": "2021-08-22",
"realtime_end": "2021-08-22",
"name": "H.15 Selected Interest Rates",
"press_release": true,
"link": "http://www.federalreserve.gov/releases/h15/"
},
{
"id": 20,
"realtime_start": "2021-08-22",
"realtime_end": "2021-08-22",
"name": "H.4.1 Factors Affecting Reserve Balances",
"press_release": true,
"link": "http://www.federalreserve.gov/releases/h41/"
},
{
"id": 95,
"realtime_start": "2021-08-22",
"realtime_end": "2021-08-22",
"name": "Manufacturer's Shipments, Inventories, and Orders (M3) Survey",
"press_release": true,
"link": "http://www.census.gov/indicator/www/m3/"
}
]
原本的series資料只有release_id去辨識,為了希望能用來源去篩選,因此要在series資料內新增source_id辨識來源:
src\data\chart-collections.json
[
{
"id": 1,
"series_id": "TREAST",
"release_id": 20,
// 新增source id
"source_id": 1,
...
},
{
"id": 2,
"series_id": "DGS10",
"release_id": 95,
// 新增source id
"source_id": 1,
...
},
{
"id": 3,
"series_id": "A34HNO",
"release_id": 18,
// 新增source id
"source_id": 19,
...
}
]
既然我們有了sources.json與releases.json,就可以把這些資料丟給React,這邊有兩個可以import資料的選擇,一個是從最上層的App.js引入,另外一個是從選單元素Selector.js引入,考量到之後可能會在除了下拉式選單的地方用到這些資料,因此選擇從App.js引入,再透過props傳遞到子元件。
從App引入資料,並透過props傳遞給Selector元件。
src\App.js
import Navbar from './components/Navbar/Navbar';
import Selector from './components/Selector/Selector';
import Charts from './components/Charts/Charts';
import chartCollections from './data/chart-collections.json';
// import 選單資料
import releases from './data/releases.json';
import sources from './data/sources.json';
function App() {
return (
<div className="App">
<Navbar />
<Selector
releases={releases}
sources={sources}
/>
<Charts charts={chartCollections} />
</div>
);
}
export default App;
在Selector元件接收props,並使用list render製作下拉式選單的option。
src\components\Selector\Selector.js
import React from 'react';
import styles from './Selector.module.css';
import Form from 'react-bootstrap/Form';
import { Row, Col } from 'react-bootstrap';
const Selector = (props) => {
return <Row className={styles.selector}>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
>
<option value={0}>All Sources</option>
{props.sources.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
>
<option value={0}>All Releases</option>
{props.releases.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
</Row>
};
export default Selector;
這次的下拉式選單功能,想做到讓使用者選取項目後,不需要再按一個submit的按鈕,下方的圖表就會自動篩選。為了要做到這個功能,首先要讓React知道使用者選了什麼選項,如果選項有改變,React也要馬上啟動篩選機制。
因此,我想到了使用state去追蹤下拉式選單的狀態,也就是讓React記住當前選到的source_id及release_id,當使用者操作下拉式選單,就會去調整對應的state,再根據調製後的state去篩選圖表。
建立filteredReleaseId與filteredSourceId兩個State,並且透過props傳遞給Selector元件。
src\App.js
...
function App() {
const [filteredReleaseId, setFilteredReleaseId] = useState(0);
const [filteredSourceId, setFilteredSourceId] = useState(0);
return (
<div className="App">
<Navbar />
<Selector
selectedReleaseId={filteredReleaseId}
selectedSourceId={filteredSourceId}
releases={releases}
sources={sources}
/>
<Charts charts={chartCollections} />
</div>
);
}
export default App;
在Form.Select設定defaultValue為props傳遞進來的資料。
src\components\Selector\Selector.js
...
const Selector = (props) => {
return <Row className={styles.selector}>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
defaultValue={props.selectedSourceId}
>
<option value={0}>All Sources</option>
{props.sources.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
defaultValue={props.selectedReleaseId}
>
<option value={0}>All Releases</option>
{props.releases.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
</Row>
};
export default Selector;
可以透過filteredReleaseId與filteredSourceId去篩選圖表資料,方式為:建立filteredCharts儲存篩選後符合條件的資料,再將其傳遞到Charts元件。
src\App.js
...
function App() {
const [filteredReleaseId, setFilteredReleaseId] = useState(0);
const [filteredSourceId, setFilteredSourceId] = useState(0);
// 篩選圖表的方式
const filteredCharts = chartCollections.filter(chart => {
if (filteredReleaseId === 0 && filteredSourceId === 0) return true
if (filteredReleaseId === 0 && filteredSourceId !== 0) {
return chart.source_id === filteredSourceId
}
if (filteredReleaseId !== 0 && filteredSourceId === 0) {
return chart.release_id === filteredReleaseId
}
if (filteredReleaseId !== 0 && filteredSourceId !== 0) {
return chart.release_id === filteredReleaseId && chart.source_id === filteredSourceId
}
});
return (
<div className="App">
<Navbar />
<Selector
selectedReleaseId={filteredReleaseId}
selectedSourceId={filteredSourceId}
releases={releases}
sources={sources}
/>
<Charts charts={filteredCharts} />
</div>
);
}
export default App;
上面的程式碼知道如何篩選圖表資料,接著,我們要讓使用者透過下拉式選單去改變filteredReleaseId與filteredSourceId這兩個state,就會促使React啟動篩選的程式。
作法是建立兩個setState函數,並將兩個函數傳遞給Selector元件,讓子元件可以呼叫函數修改父元件的state,這個過程稱為資料的逆向傳遞。
src\App.js
...
function App() {
const [filteredReleaseId, setFilteredReleaseId] = useState(0);
const [filteredSourceId, setFilteredSourceId] = useState(0);
const filteredCharts = chartCollections.filter(chart => {
if (filteredReleaseId === 0 && filteredSourceId === 0) return true
if (filteredReleaseId === 0 && filteredSourceId !== 0) {
return chart.source_id === filteredSourceId
}
if (filteredReleaseId !== 0 && filteredSourceId === 0) {
return chart.release_id === filteredReleaseId
}
if (filteredReleaseId !== 0 && filteredSourceId !== 0) {
return chart.release_id === filteredReleaseId && chart.source_id === filteredSourceId
}
});
// setState函數
const releaseIdChangeHandler = (selectedReleaseId) => {
setFilteredReleaseId(selectedReleaseId);
};
// setState函數
const sourceIdChangeHandler = (selectedSourceId) => {
setFilteredSourceId(selectedSourceId);
};
return (
<div className="App">
<Navbar />
<Selector
selectedReleaseId={filteredReleaseId}
selectedSourceId={filteredSourceId}
releases={releases}
sources={sources}
onReleaseIdChange={releaseIdChangeHandler}
onSourceIdChange={sourceIdChangeHandler}
/>
<Charts charts={filteredCharts} />
</div>
);
}
export default App;
在Selector的Form.Select元件內新增onChange事件,再透過父元件傳遞進來的事件將篩選到的值逆向傳遞回去。這邊要注意的是子元件不能直接修改父元件的state,但是可以透過props將資料逆向傳遞回去,透過父元件的setState函數去修改父元件的state。
src\components\Selector\Selector.js
...
const Selector = (props) => {
const releaseIdChange = (event) => {
props.onReleaseIdChange(Number(event.target.value));
};
const sourceIdChange = (event) => {
props.onSourceIdChange(Number(event.target.value));
};
return <Row className={styles.selector}>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
defaultValue={props.selectedSourceId}
onChange={sourceIdChange}
>
<option value={0}>All Sources</option>
{props.sources.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
<Col sm={12} md={6} lg={4} className={styles.selectorItem}>
<Form.Select
aria-label="Default select example"
defaultValue={props.selectedReleaseId}
onChange={releaseIdChange}
>
<option value={0}>All Releases</option>
{props.releases.map((e) => (
<option value={e.id} key={e.id}>{e.name}</option>
))}
</Form.Select>
</Col>
</Row>
};
export default Selector;
看似簡單的篩選功能,其實也是蠻多程式碼的,不過藉此也學到資料如何在父元件與子元件間傳遞,算是React內非常基本的知識。
雖然目前有了篩選功能,但是當圖表越來越多張的時候,還是可能讓畫面變得很長,下一篇就來解決這個問題,最基本的方式應該就是用分頁來處理。