A list page is the most common scenario in real development. A list page is a collection of data, with each entry leading to a detailed page. Key technical points to consider in developing a list:
-
How to turn pages: During page turning, is the data source server or client?
-
How to search for content: front-end search or server-side search (send requests)
-
How to cache data: return a list page from a content page, data from a front-end cache
-
How to refresh the page: Refresh cached data when data is modified
The design of the store
Page data and operation management will be placed in store, so we first design a Store model:
const initialState = {
listItems: [].// array
keyword: ' '.// string
page: 1.// number
pageSize: 3.// number
total: 0.// number
byId: {}, // object
/* ** Data request related ⬇️ */
fetchListPending: false.// Boolean, in request
fetchListError: null.// object, request failure information
listNeedReload: false.// Boolean, whether to request data again
};
Copy the code
In response to a flat structure advocated by Redux, we store a set of ids in listItems rather than all the data, which is retrieved by byId.
The design of the URL
To increase the user experience, we typically map a unique URL for each resource, including the current page and keyword as part of the URL:
/list/${page}? keyword=${XXX}Copy the code
<Switch>
<Route path="/table/:page?">
<Table />
</Route>
<Route path="/user/:userId">
<Detail />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
Copy the code
implementation
directory
├ ─ ─ App. Js ├ ─ ─ the SRC ├ ─ ─ store ├ ─ ─ action. Js ├ ─ ─ reducer. Js └ ─ ─ store. Js └ ─ ─ pages ├ ─ ─ detail. Js └ ─ ─ table. JsCopy the code
The UI framework
Based on Ant Design, we mainly used Input, Table and Pagination components, of which Pagination has been encapsulated by Table.
- table.js
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;
Copy the code
- detail.js
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;
Copy the code
Store implementation (asynchronous Action)
We use the REQ | RES (reqres in/API/users? P… As test data, we set up at least three actions for data requests:
'FETCH_LIST_BEGIN'
: Request start'FETCH_LIST_SUCCESS'
: Request successful'FETCH_LIST_ERROR'
: Request failed
We will also use dependencies:
- Use AXIos to send the request
- Use redux-thunk to handle asynchronous actions
- Use the Redux-Logger to print and dispatch action logs for us
This article covers asynchronous actions in more detail.
action.js
import axios from 'axios';
// Get the user list
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); }); }); };// Get user information
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); }); }); };Copy the code
reducer.js
Handle state according to action
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;
}
Copy the code
store.js
Create Store + Setup middleware
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 });
// Set up the debugging tool
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
// Set up middleware
const enhancer = composeEnhancers(applyMiddleware(thunk, logger));
// Create store
const store = createStore(reducer, enhancer);
export default store;
Copy the code
List of pp.
Page data loading
The previously written component is only a presentation component (responsible for rendering the UI), and to use a Store in a component you need to wrap a container component around it. Redux uses React to develop applications.
connect store
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);
Copy the code
Get/process data
// something import ....
const TablePage = (props) = > {
const { items, byId, fetchList, page, total, pageSize } = props;
// Process data
const getDataSource = () = > {
if(! items)return [];
return items.map((id) = > byId[id]);
};
// Get data
useEffect(() = > {
fetchList(1); } []);/ / rendering 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>
);
};
Copy the code
Turn the page
Our page-turning state can be saved in the route and still stay at the current page number after refreshing
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 the page number does not change, the request will not be renewed
if(page ! == initPage) fetchList(parseInt(initPage, 10));
// eslint-disable-next-line} []);// Handle page number changes
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>
);
};
Copy the code
Handling load state
-
Global loading during the first rendering
// The page has no data yet or the data is empty if(! items || ! items.length)return 'loading... '; Copy the code
-
Partial loading after the first rendering
const { fetchListPending } = props; return <Table loading={fetchListPending} />; Copy the code
Update the data cache
When we cut back from the content page to the list page, use cached data:
if(page ! == initPage || ! getDataSource().length) fetchList(parseInt(initPage, 10));
Copy the code
Remember the listNeedReload field in initState, which we used to determine whether to update the cache.
If you modify data on the content page (detail.js), you should also set listNeedReload = true
useEffect(() = > {
const initPage = routerPage || 1;
if(page ! == initPage || ! getDataSource().length || listNeedReload) fetchList(parseInt(initPage, 10)); } []);Copy the code
Error handling
If there is an error message, the page is hijacked without subsequent rendering.
// pages/table.js
const { fetchListError } = porps;
if (fetchListError) {
return <div>{fetchListError.error.message}</div>;
}
Copy the code
table.js
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;
/ / page number change | | not pull data | | need to 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);
Copy the code
Content page
Content pages and list pages have two data relationships:
- Simple data: The list page data contains the content page data and does not need to be refetched (note the case of going directly to the content page)
- Complex data: Content page data requires additional retrieval
Well, the first case covers the second case.
First, determine whether user data exists in the store. If not, initiate a fetch request.
import { fetchUser } from '.. /store/action/table';
const Detail = (props) = > {
const { byId, fetchUser } = props;
const user = byId ? byId[userId] : null;
useEffect(() = > {
if (!user) fetchUser(userId);
}, []);
};
Copy the code
detail.js
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);
Copy the code
React Best Practices
- React Best practice: Create a drag-and-drop list by hand
- React Best Practice: Integrating third-party Libraries (d3.js)
- React Best Practices: How to Implement Native Portals
- React best practice: Drag-and-drop sidebar
- React best practice: Implement step-by-step operations based on routes
- React best practice: Work with multiple data sources
- React best practice: Complete a list of requirements
- React best practice: Dynamic forms