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:

  1. How to turn pages: During page turning, is the data source server or client?

  2. How to search for content: front-end search or server-side search (send requests)

  3. How to cache data: return a list page from a content page, data from a front-end cache

  4. 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