列表頁面 是實際開發中最多見的場景,列表頁面是一堆數據的集合,經過每一條記錄能夠進入對應的詳細頁面。開發列表主要須要考慮的技術點:前端
如何翻頁:翻頁過程當中,數據的來源是服務器端仍是客戶端?react
如何進行內容搜索:前端搜索 or 服務器端搜索(發送請求)ios
如何緩存數據:從內容頁返回列表頁,數據來自於前端緩存redux
如何進行頁面的刷新:數據發生修改時,刷新緩存數據axios
頁面的數據,以及操做的管理咱們都會放在 store 裏,因此咱們先設計一個 store 模型:api
const initialState = {
listItems: [], // array
keyword: '', // string
page: 1, // number
pageSize: 3, // number
total: 0, // number
byId: {}, // object
/* ** 數據請求相關 ⬇️ */
fetchListPending: false, // boolean,請求中
fetchListError: null, // object,請求失敗信息
listNeedReload: false, // boolean,是否須要從新請求數據
};
複製代碼
響應 redux 推崇的一種扁平化結構:咱們在 listItems
中保存的是一組 id,而非所有數據,具體數據經過 byId
獲取。緩存
爲了增長用戶體驗,咱們一般都會爲每個資源映射一個惟一的 URL,將當前頁面和關鍵字 keyword 也做爲 URL 的一部分:服務器
/list/${page}?keyword=${XXX}
複製代碼
<Switch>
<Route path="/table/:page?"> <Table /> </Route>
<Route path="/user/:userId"> <Detail /> </Route>
<Route path="/"> <Home /> </Route>
</Switch>
複製代碼
├── App.js
├── src
├── store
├── action.js
├── reducer.js
└── store.js
└── pages
├── detail.js
└── table.js
複製代碼
基於 Ant Design
,咱們主要用到 Input, Table, Pagination
三個組件,其中 Pagination
已經被 Table
封裝自帶了。markdown
import { Input, Table } from 'antd';
const { Search } = Input;
const { Column, ColumnGroup } = Table;
const TablePage = () => {
return (
<div> <Search placeholder="Search..." style={{ width: '200px' }} /> <Table style={{ width: '800px', margin: '50px auto' }} rowKey="id" pagination={{ position: 'bottomCenter' }} > <Column title="ID" dataIndex="id" key="id" /> <ColumnGroup title="Name"> <Column title="First Name" dataIndex="first_name" key="first_name" /> <Column title="Last Name" dataIndex="last_name" key="last_name" /> </ColumnGroup> <Column title="Email" dataIndex="email" key="email" /> </Table> <br /> </div>
);
};
export default TablePage;
複製代碼
function Detail() {
return (
<div className="detail-page"> <Link to="/table">Back to list</Link> <ul> <li> <label>First name:</label> <span></span> </li> <li> <label>Last name:</label> <span></span> </li> </ul> </div>
);
}
export default Detail;
複製代碼
咱們用 REQ | RES( reqres.in/api/users?p… )做爲測試數據,關於數據請求咱們至少設置三種 action:antd
'FETCH_LIST_BEGIN'
:請求開始'FETCH_LIST_SUCCESS'
:請求成功'FETCH_LIST_ERROR'
:請求失敗咱們還會用到的依賴:
關於 異步 action 更多內容,這篇文章 作了詳細介紹。
import axios from 'axios';
// 獲取用戶列表
export const fetchList =
(page = 1, pageSize = 3, keyword = '') =>
(dispatch) => {
dispatch({
type: 'FETCH_LIST_BEGIN',
});
return new Promise((resolve, reject) => {
const doRequest = axios.get(
`https://reqres.in/api/users?page=${page}&per_page=${pageSize}&q=${keyword}`,
);
doRequest.then(
(res) => {
dispatch({
type: 'FETCH_LIST_SUCCESS',
data: {
items: res.data.data,
page,
pageSize,
total: res.data.total,
},
});
resolve(res);
},
(err) => {
dispatch({
type: 'FETCH_LIST_ERROR',
data: { error: err },
});
reject(err);
},
);
});
};
// 獲取用戶具體信息
export const fetchUser = (id) => (dispatch) => {
dispatch({
type: 'FETCH_USER_BEGIN',
});
return new Promise((resolve, reject) => {
const doRequest = axios.get(`https://reqres.in/api/users/${id}`);
doRequest.then(
(res) => {
dispatch({
type: 'FETCH_USER_SUCCESS',
data: res.data.data,
});
resolve(res);
},
(err) => {
dispatch({
type: 'FETCH_USER_ERROR',
data: { error: err },
});
reject(err);
},
);
});
};
複製代碼
根據 action 處理 state
const initialState = {
items: [],
page: 1,
pageSize: 3,
total: 0,
byId: {},
fetchListPending: false,
fetchListError: null,
fetchUserPending: false,
fetchUserError: null,
listNeedReload: false,
};
// reducer
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_LIST_BEGIN':
return {
...state,
fetchListPending: true,
fetchListError: null,
};
case 'FETCH_LIST_SUCCESS': {
const byId = {};
const items = [];
action.data.items.forEach((item) => {
items.push(item.id);
byId[item.id] = item;
});
return {
...state,
byId,
items,
page: action.data.page,
pageSize: action.data.pageSize,
total: action.data.total,
fetchListPending: false,
fetchListError: null,
};
}
case 'FETCH_LIST_ERROR':
return {
...state,
fetchListPending: false,
fetchListError: action.data,
};
case 'FETCH_USER_BEGIN':
return {
...state,
fetchUserPending: true,
fetchUserError: null,
};
case 'FETCH_USER_SUCCESS': {
return {
...state,
byId: {
...state.byId,
[action.data.id]: action.data,
},
fetchUserPending: false,
};
}
case 'FETCH_USER_ERROR':
return {
...state,
fetchUserPending: false,
fetchUserError: action.data,
};
default:
break;
}
return state;
}
複製代碼
create store + 設置中間件
import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';
const createLogger = require('redux-logger').createLogger;
const logger = createLogger({ collapsed: true });
// 設置調試工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
// 設置中間件
const enhancer = composeEnhancers(applyMiddleware(thunk, logger));
// Create store
const store = createStore(reducer, enhancer);
export default store;
複製代碼
前面寫到的組件只能算是一個展現組件(負責 UI 的呈現),想要在組件中使用 store 就須要用一個容器組件包裹它。詳細介紹看 這篇 --- Redux 結合 React 開發應用。
import { connect } from 'react-redux';
import { fetchList, fetchUser } from '../store/action';
const TablePage = (props) => {
return;
};
const mapStateToProps = function (state) {
return {
...state.table,
};
};
const mapDispatchToProps = { fetchList, fetchUser };
export default connect(mapStateToProps, mapDispatchToProps)(TablePage);
複製代碼
// something import ....
const TablePage = (props) => {
const { items, byId, fetchList, page, total, pageSize } = props;
// 處理數據
const getDataSource = () => {
if (!items) return [];
return items.map((id) => byId[id]);
};
// 獲取數據
useEffect(() => {
fetchList(1);
}, []);
// 渲染 UI
return (
<div> // ... <Table dataSource={getDataSource()} style={{ width: '800px', margin: '50px auto' }} rowKey="id" pagination={{ current: page, total: total, pageSize: pageSize, }} > <Column title="ID" dataIndex="id" key="id" render={(id) => <Link to={`/user/${id}`}>{id}</Link>} /> <ColumnGroup title="Name"> <Column title="First Name" dataIndex="first_name" key="first_name" /> <Column title="Last Name" dataIndex="last_name" key="last_name" /> </ColumnGroup> <Column title="Email" dataIndex="email" key="email" /> </Table> </div>
);
};
複製代碼
咱們的翻頁狀態是能夠保存在路由中的,刷新後依然停留在當前頁碼
import { useHistory, useParams } from 'react-router-dom';
const TablePage = (props) => {
let history = useHistory();
const { page: routerPage } = useParams();
const { page } = props;
useEffect(() => {
const initPage = routerPage || 1;
// 頁碼無變化時,不會從新請求
if (page !== initPage) fetchList(parseInt(initPage, 10));
// eslint-disable-next-line
}, []);
// 處理頁碼變化
const handlePageChange = (newPage) => {
history.push(`/table/${newPage}`);
fetchList(newPage);
};
return (
<div> // ... <Table dataSource={getDataSource()} pagination={{ current: page, onChange: handlePageChange, total: total, pageSize: pageSize, }} > // ... </Table> </div>
);
};
複製代碼
第一次渲染時的全局 loading
// 頁面尚未數據 或 數據爲空
if (!items || !items.length) return 'loading...';
複製代碼
第一次渲染後的局部 loading
const { fetchListPending } = props;
return <Table loading={fetchListPending} />;
複製代碼
當咱們從內容頁切回列表頁,使用緩存數據:
if (page !== initPage || !getDataSource().length)
fetchList(parseInt(initPage, 10));
複製代碼
還記得咱們 initState 中的 listNeedReload 字段嗎,它是咱們判斷是否更新緩存的依據。
若是在內容頁(detail.js)發生了修改數據的操做,應該同時設定 listNeedReload = true
useEffect(() => {
const initPage = routerPage || 1;
if (page !== initPage || !getDataSource().length || listNeedReload)
fetchList(parseInt(initPage, 10));
}, []);
複製代碼
若是存在錯誤信息,就劫持頁面不進行後續的渲染。
// pages/table.js
const { fetchListError } = porps;
if (fetchListError) {
return <div>{fetchListError.error.message}</div>;
}
複製代碼
import { useState, useEffect } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { Input, Table } from 'antd';
import { fetchList, fetchUser } from '../store/action/table';
const { Search } = Input;
const { Column, ColumnGroup } = Table;
const TablePage = (props) => {
let history = useHistory();
const { page: routerPage } = useParams();
const [search, setSearch] = useState('');
const {
items,
byId,
fetchList,
fetchListError,
fetchListPending,
page,
total,
pageSize,
listNeedReload,
} = props;
const getDataSource = () => {
if (!items) return [];
return items.map((id) => byId[id]);
};
useEffect(() => {
const initPage = routerPage || 1;
// 頁碼變化 || 未拉取過數據 || 須要 reload
if (page !== initPage || !getDataSource().length || listNeedReload)
fetchList(parseInt(initPage, 10));
// eslint-disable-next-line
}, []);
if (fetchListError) {
return <div>{fetchListError.error.message}</div>;
}
if (!items || !items.length) return 'loading...';
const handlePageChange = (newPage) => {
history.push(`/table/${newPage}`);
fetchList(newPage);
};
const handleSearch = (keyword) => {
fetchList(page, pageSize, keyword);
};
return (
<div> <Search placeholder="Search..." style={{ width: '200px' }} value={search} onChange={(e) => setSearch(e.target.value)} onSearch={handleSearch} /> <Table dataSource={getDataSource()} style={{ width: '800px', margin: '50px auto' }} rowKey="id" loading={fetchListPending} pagination={{ current: page, onChange: handlePageChange, total: total, pageSize: pageSize, }} > <Column title="ID" dataIndex="id" key="id" render={(id) => <Link to={`/user/${id}`}>{id}</Link>} /> <ColumnGroup title="Name"> <Column title="First Name" dataIndex="first_name" key="first_name" /> <Column title="Last Name" dataIndex="last_name" key="last_name" /> </ColumnGroup> <Column title="Email" dataIndex="email" key="email" /> </Table> </div>
);
};
const mapStateToProps = function (state) {
return {
...state.table,
};
};
const mapDispatchToProps = { fetchList, fetchUser };
export default connect(mapStateToProps, mapDispatchToProps)(TablePage);
複製代碼
內容頁和列表頁存在兩種數據關係:
其實第一種狀況能夠涵蓋第二種狀況了。
首先,判斷 store 中是否存在 user 數據;若是不存在,發起 fetch 請求。
import { fetchUser } from '../store/action/table';
const Detail = (props) => {
const { byId, fetchUser } = props;
const user = byId ? byId[userId] : null;
useEffect(() => {
if (!user) fetchUser(userId);
}, []);
};
複製代碼
import { useEffect } from 'react';
import { connect } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { fetchUser } from '../store/action/table';
function Detail(props) {
const { userId } = useParams();
const { byId, fetchUserPending, fetchUser } = props;
const user = byId ? byId[userId] : null;
useEffect(() => {
if (!user) fetchUser(userId);
// eslint-disable-next-line
}, []);
if (!user || fetchUserPending) return 'loading...';
const { first_name, last_name } = user;
return (
<div className="detail-page"> <Link to="/table">Back to list</Link> <ul> <li> <label>First name:</label> <span>{first_name}</span> </li> <li> <label>Last name:</label> <span>{last_name}</span> </li> </ul> </div>
);
}
function mapStateToProps(state) {
return {
...state.table,
};
}
const mapDispatchToProps = { fetchUser };
export default connect(mapStateToProps, mapDispatchToProps)(Detail);
複製代碼