This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

Code hits bottom to load, life hits bottom to bounce.

preface

We often encounter rendering lists in page development. There are usually two modes: switching pages and infinite append, which is usually done with a bottom-hitting hook (callback function). For small programs, there is a special bottom hook (lifecycle). But if it’s non-mobile, we need to implement the function to determine if we’ve hit the bottom. React Ù©(‘ω’)Ùˆ

1. Business scenarios

There is a list page that needs to append the next page’s data to the page when the page hits the bottom.

Second, implementation ideas

  • Encapsulate a function that listens for the bottom of the page.
  • The next page of data is requested when the bottom is reached and appended to the array to render.
  • Further optimize the code.

Three, coding

3.1 Requesting Data

First we need to write the method that requests the data. Here we use useRequest of ahooks. (Because scaffolding UmiJS has this function from ahooks built in, we import it from UMI.)

  • UmiJS document
  • Ahooks document

Global utility functions utils/index.js

// utils/index.js

/** * if an array is passed in, return an empty array otherwise@param    Data Specifies the incoming data to be processed *@returns  Array* /
export const wantArray = (data) = > (Array.isArray(data) ? data : []);
Copy the code

Encapsulate the request’s service.js

// service.js
import { request } from 'umi';

// List of announcements
export function getNoticeList(params) {
  // XXX is the requested address
  return request('xxx', { params });
};
Copy the code

The key code of the JSX file of the announcement list has been annotated and can be safely eaten.

// Notice list JSX
// The following is the key code
import { useEffect, useState } from 'react';
import { useRequest } from 'umi';
import { Card, Spin } from 'antd';

import ArticleItem from '@/components/ArticleItem';// Article entry component
import { wantArray } from '@/utils';
import { getNoticeList } from './service';

export default() = > {/ * = = = = = = = = = = = = = = = = = = = = = = = = announcement list = = = = = = = = = = = = = = = = = = = = = = = = * /
    const pageSize = 10;// The number of lines per page is const because we don't need to change this
    const [current, setCurrent] = useState(1);// Current page number
    const [list, setList] = useState([]);// List array

    // todo requests data
    const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, {
        manual: true.// Enable manual request
        formatResult: res= > {// Format the datasetCurrent(res? .current);// Set the current page number
            setList([...list, ...wantArray(res?.data)]);// Append an array}});// After entering the page, data is requested once by default
    useEffect(() = > { runGetNoticeList({ current, pageSize }) }, []);
    
    // omit other code...
    
    return (
        <>{/* omit other code... * /}<Card tabList={[{ key:"',tab:'Notice'}]}>
                <Spin size="large" spinning={noticeListLoading} tip="Loading...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value? .id}
                                    item={value}
                                />); })}</Spin>
            </Card>
        </>
    );
};
Copy the code

3.2 Whether the bottom has been reached

We need to implement a function that determines whether the bottom has been reached and throttle it. Finally, listen with addEventListener (which needs to be destroyed when the component is destroyed).

// Notice list JSX
// The following is the key code
import { useEffect } from "react";
import { message } from "antd";
import { throttle } from "lodash";

export default() = > {/** * load more ** this function does the interface request etc */
    const handleLoadMore = () = > {
        // Temporarily use message for testing effects
        message.info("Hit the bottom.");
    };

    /** * this function determines whether the bottom has been reached@param    Handler Specifies the callback function * to be executed@returns  null* /
    const isTouchBottom = (handler) = > {
        // Document displays area height
        const showHeight = window.innerHeight;
        // Page curl height
        const scrollTopHeight =
            document.body.scrollTop || document.documentElement.scrollTop;
        // All content height
        const allHeight = document.body.scrollHeight;
        // (all content height = document display area height + page curl height)
        if (allHeight <= showHeight + scrollTopHeight) {
            handler();
        };
    };

    /** * Throttling determines whether the bottom is reached@returns  function* /
    const useFn = throttle(() = > {
        // Call load more functions here
        isTouchBottom(handleLoadMore);
    }, 500);

    useEffect(() = > {
        // Start a listener to listen for page scrolling
        window.addEventListener("scroll", useFn);

        // Remove the listener when the component is destroyed
        return () = > { window.removeEventListener("scroll", useFn) }; } []);// omit other code...
};
Copy the code

Let’s take a look at the effect first:

Okay, so let’s move on to the next step.

3.3 Bottom loading

We just need to request data when we hit bottom. One problem here is that listeners in functional components cannot get the variables that are updated in real time. This is assisted by useRef. This was one of the problems I encountered during development, which was solved by reading the article “How to get the latest state in the React Listener execution Method”.

// Notice list JSX
// The following is the key code
import { useEffect, useState, useRef } from 'react';
import { useRequest } from 'umi';
import { Card, Spin, message } from 'antd';

import ArticleItem from '@/components/ArticleItem';// Article entry component
import { throttle } from 'lodash';
import { wantArray } from '@/utils';
import { getNoticeList } from './service';

export default() = > {/ * = = = = = = = = = = = = = = = = = = = = = = = = announcement list = = = = = = = = = = = = = = = = = = = = = = = = * /
    const pageSize = 10;
    const [current, setCurrent] = useState(1);
    const [list, setList] = useState([]);
    // Add a variable here to store whether there is more data
    const [isMore, setIsMore] = useState(true);

    // todo requests data
    const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, {
        manual: true.formatResult: res= >{ setCurrent(res? .current);// Set the current page number
            setList([...list, ...wantArray(res?.data)]);// Append an array
            // If the current page number is greater than or equal to the total number of pages, sets whether there is more data to false
            if (current >= Math.round(res.total / pageSize)) { setIsMore(false)}; }});// After entering the page, data is requested once by default
    useEffect(() = > { runGetNoticeList({ current, pageSize }) }, []);
    
    // To solve the problem that the listener cannot get the latest state value, use useRef instead of state
    const currentRef = useRef(null);
    useEffect(() = > { currentRef.current = current }, [current]);
    const loadingRef = useRef(null);
    useEffect(() = > { loadingRef.current = noticeListLoading }, [noticeListLoading]);
    const isMoreRef = useRef(null);
    useEffect(() = > { isMoreRef.current = isMore }, [isMore]);
    
    // Todo loads more
    const handleLoadMore = () = > {
        if(! loadingRef.current && isMoreRef.current) { message.info('Load next page ~');
            
            // Create a temporary variable in case (current + 1) is not updated in time
            const temp = currentRef.current + 1;
            setCurrent(temp);
            runGetNoticeList({ current: temp, pageSize });
        };
    };
    
    // Todo determines if the bottom has been reached
    const isTouchBottom = (handler) = > {
        // Document displays area height
        const showHeight = window.innerHeight;
        // Page curl height
        const scrollTopHeight =
            document.body.scrollTop || document.documentElement.scrollTop;
        // All content height
        const allHeight = document.body.scrollHeight;
        // (all content height = document display area height + page curl height)
        if (allHeight <= showHeight + scrollTopHeight) {
            handler();
        };
    };
    
    const useFn = throttle(() = > {
        // Call load more functions here
        isTouchBottom(handleLoadMore);
    }, 500);

    useEffect(() = > {
        // Start a listener to listen for page scrolling
        window.addEventListener("scroll", useFn);

        // Remove the listener when the component is destroyed
        return () = > { window.removeEventListener("scroll", useFn) }; } []);return (
        <>{/* omit other code... * /}<Card tabList={[{ key:"',tab:'Notice'}]}>
                <Spin size="large" spinning={noticeListLoading} tip="Loading...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value? .id}
                                    item={value}
                                />); })}</Spin>
            </Card>
        </>
    );
};
Copy the code

Let’s take a look at the effect first:

Although the functionality is implemented, it still needs to be optimized.

3.4 Package bottom loading hook

If multiple pages use the bottom-loading feature, it needs to be wrapped, because it is a piece of code and does not contain the UI part, so it is wrapped as a hook. Create a new folder in the SRC directory and name it hooks, then create a new folder useTouchBottom and create index.js in it.

// src/hooks/useTouchBottom/index.js
// Load the hook
import { useEffect } from 'react';
import { throttle } from 'lodash';

const isTouchBottom = (handler) = > {
  // Document displays area height
  const showHeight = window.innerHeight;
  // Page curl height
  const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop;
  // All content height
  const allHeight = document.body.scrollHeight;
  // (all content height = document display area height + page curl height)
  if(allHeight <= showHeight + scrollTopHeight) { handler(); }};const useTouchBottom = (fn) = > {
  const useFn = throttle(() = > {
    if (typeof fn === 'function') {
      isTouchBottom(fn);
    };
  }, 500);

  useEffect(() = > {
    window.addEventListener('scroll', useFn);
    return () = > {
      window.removeEventListener('scroll', useFn); }; } []); };export default useTouchBottom;
Copy the code
// Notice list JSX
// The following is the key code
import { useEffect, useState, useRef } from 'react';
import { useRequest } from 'umi';
import { Card, Spin, message } from 'antd';

import ArticleItem from '@/components/ArticleItem';// Article entry component
import useTouchBottom from '@/hooks/useTouchBottom';// Load the hook
import { wantArray } from '@/utils';
import { getNoticeList } from './service';

export default() = > {/ * = = = = = = = = = = = = = = = = = = = = = = = = announcement list = = = = = = = = = = = = = = = = = = = = = = = = * /
    // ...
    
    // To solve the problem that the listener cannot get the latest state value, use useRef instead of state
    // ...
    
    // Todo loads more
    const handleLoadMore = () = > {
        if(! loadingRef.current && isMoreRef.current) { message.info('Load next page ~');
            
            const temp = currentRef.current + 1;
            setCurrent(temp);
            runGetNoticeList({ current: temp, pageSize });
        };
    };
    
    // Use bottom loading hook
    useTouchBottom(handleLoadMore);
    
    // omit other code...
};
Copy the code

3.5 Encapsulation Loading More Components

We can find that the loading is a bit stiff and need a load more components to save the field. Here is the simplest version.

// src/components/LoadMore/index.jsx
// Load more components
import styles from './index.less';

/ * * *@param  The status status loadmore | loading | nomore *@param  Hidden Whether to hide */
const LoadMore = ({ status = 'loadmore', hidden = false }) = > {
  return (
    <div className={styles.loadmore} hidden={hidden}>
      {status === 'loadmore' && <div>The drop-down load</div>}
      {status === 'loading' && <div>Loading in...</div>}
      {status === 'nomore' && <div>All content has been loaded</div>}
    </div>
  );
};

export default LoadMore;
Copy the code
// src/components/LoadMore/index.less
// Load more components
.loadmore {
  padding: 12px 0;
  width: 100%;
  color: rgba(0.0.0.0.6);
  font-size: 14px;
  text-align: center;
}
Copy the code
// Notice list JSX
// The following is the key code
import { useEffect, useState, useRef } from 'react';
import { useRequest } from 'umi';
import { Card, Spin, message } from 'antd';

import ArticleItem from '@/components/ArticleItem';// Article entry component
import LoadMore from '@/components/LoadMore'; // Load more components
import useTouchBottom from '@/hooks/useTouchBottom';// Load the hook
import { wantArray } from '@/utils';
import { getNoticeList } from './service';

export default() = > {/ * = = = = = = = = = = = = = = = = = = = = = = = = announcement list = = = = = = = = = = = = = = = = = = = = = = = = * /
    // Create a new variable to hold the status of loading more components. The initial value is loadMore
    const [loadMoreStatus, setLoadMoreStatus] = useState('loadmore');
    // ...
    
    // To solve the problem that the listener cannot get the latest state value, use useRef instead of state
    const currentRef = useRef(null);
    useEffect(() = > { currentRef.current = current }, [current]);
    
    // You need to change the loadMoreStatus status when loading and isMore change
    const loadingRef = useRef(null);
    useEffect(() = > {
        loadingRef.current = noticeListLoading;
        if (noticeListLoading) { setLoadMoreStatus('loading')}; }, [noticeListLoading]);const isMoreRef = useRef(null);
    useEffect(() = > {
        if(! isMore) { setLoadMoreStatus('nomore')}; isMoreRef.current = isMore; }, [isMore]);// omit other code...
    
    return (
        <>{/* omit other code... * /}<Card tabList={[{ key:"',tab:'Notice'}]}>
                <Spin size="large" spinning={noticeListLoading} tip="Loading...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value? .id}
                                    item={value}
                                />); /* Load more components */}<LoadMore status={loadMoreStatus} hidden={list.length= = =0} />
                </Spin>
            </Card>
        </>
    );
};
Copy the code

3.6 Encapsulating null State Components

For lists, we typically need to define an empty state component to default placeholders.

// src/components/Empty/index.jsx
// Empty state component
import styles from './index.less';
import emptyList from '@/assets/images/common/empty-list.svg';

const Empty = (() = > {
    return (
        <div className={styles.empty}>
            <img src={emptyList} alt="No data at present" />
            <div>Temporarily no data</div>
        </div>
    );
});

export default Empty;
Copy the code
// src/components/Empty/index.less
// Empty state component
.empty {
  padding: 50px 0;
  color: rgba(0.0.0.0.65);
  font-size: 14px;
  text-align: center;

  img {
    margin-bottom: 16px; }}Copy the code
// Notice list JSX
// The following is the key code
import Empty from '@/components/Empty';/ /? Null state component

export default() = > {// omit other code...
    
    return (
        <>{/* omit other code... * /}<Card tabList={[{ key:"',tab:'Notice'}]}>
                <Spin size="large" spinning={noticeListLoading} tip="Loading...">{/* Empty state component */} {list.length === 0 &&<Empty />}
                    
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value? .id}
                                    item={value}
                                />); })}<LoadMore status={loadMoreStatus} hidden={list.length= = =0} />
                </Spin>
            </Card>
        </>
    );
};
Copy the code

When the list is empty, it looks like this:

3.7 Problem: Page zooming

When tested, there was a problem with the code above: when the page was zoomed, the function to determine if it had hit the bottom failed. After investigation, it is found that the page crimp height and all content height will change when the page is zooming, and the equation page crimp height + page crimp height = all content height is no longer valid. One current solution is to change the judgment to all content height <= document display area height + page crimp height + 100, that is:

// src/hooks/useTouchBottom/index.js
// Load the hook

const isTouchBottom = (handler) = > {
  // Document displays area height
  const showHeight = window.innerHeight;
  // Page curl height
  const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop;
  // All content height
  const allHeight = document.body.scrollHeight;
  // (all content height = document display area height + page curl height)
  // Determine all content height <= document display area height + page curl height + 100
  if (allHeight <= showHeight + scrollTopHeight + 100) { handler(); }};Copy the code

3.8 Complete Code

// utils/index.js

/** * if an array is passed in, return an empty array otherwise@param    Data Specifies the incoming data to be processed *@returns  Array* /
export const wantArray = (data) = > (Array.isArray(data) ? data : []);
Copy the code
// service.js
import { request } from 'umi';

// List of announcements
export function getNoticeList(params) {
  // XXX is the requested address
  return request('xxx', { params });
};
Copy the code
// src/components/LoadMore/index.jsx
// Load more components
import styles from './index.less';

/ * * *@param  The status status loadmore | loading | nomore *@param  Hidden Whether to hide */
const LoadMore = ({ status = 'loadmore', hidden = false }) = > {
  return (
    <div className={styles.loadmore} hidden={hidden}>
      {status === 'loadmore' && <div>The drop-down load</div>}
      {status === 'loading' && <div>Loading in...</div>}
      {status === 'nomore' && <div>All content has been loaded</div>}
    </div>
  );
};

export default LoadMore;
Copy the code
// src/components/LoadMore/index.less
// Load more components
.loadmore {
  padding: 12px 0;
  width: 100%;
  color: rgba(0.0.0.0.6);
  font-size: 14px;
  text-align: center;
}
Copy the code
// src/components/Empty/index.jsx
// Empty state component
import styles from './index.less';
import emptyList from '@/assets/images/common/empty-list.svg';

const Empty = (() = > {
    return (
        <div className={styles.empty}>
            <img src={emptyList} alt="No data at present" />
            <div>Temporarily no data</div>
        </div>
    );
});

export default Empty;
Copy the code
// src/components/Empty/index.less
// Empty state component
.empty {
  padding: 50px 0;
  color: rgba(0.0.0.0.65);
  font-size: 14px;
  text-align: center;

  img {
    margin-bottom: 16px; }}Copy the code
// Notice list JSX
import { useEffect, useState, useRef } from 'react';
import { useRequest } from 'umi';
import { Card, Spin } from 'antd';

import ArticleItem from '@/components/ArticleItem';// Article entry component
import Empty from '@/components/Empty';/ /? Null state component
import LoadMore from '@/components/LoadMore'; // Load more components
import useTouchBottom from '@/hooks/useTouchBottom';// Load the hook

import { wantArray } from '@/utils';
import { getNoticeList } from './service';

const Notice = () = > {
  / * = = = = = = = = = = = = = = = = = = = = = = = = announcement list = = = = = = = = = = = = = = = = = = = = = = = = * /
  const pageSize = 10;
  const [current, setCurrent] = useState(1);
  const [list, setList] = useState([]);
  const [isMore, setIsMore] = useState(true);
  const [loadMoreStatus, setLoadMoreStatus] = useState('loadmore');

  // todo requests data
  const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, {
    manual: true.formatResult: res= >{ setCurrent(res? .current); setList([...list, ...wantArray(res?.data)]);if (current >= Math.round(res.total / pageSize)) { setIsMore(false)}; }}); useEffect(() = > { runGetNoticeList({ current, pageSize }) }, []);

  // To solve the problem that the listener cannot get the latest state value, use useRef instead of state
  const currentRef = useRef(null);
  useEffect(() = > { currentRef.current = current }, [current]);
  const loadingRef = useRef(null);
  useEffect(() = > {
    loadingRef.current = noticeListLoading;
    if (noticeListLoading) { setLoadMoreStatus('loading')}; }, [noticeListLoading]);const isMoreRef = useRef(null);
  useEffect(() = > {
    if(! isMore) { setLoadMoreStatus('nomore')}; isMoreRef.current = isMore; }, [isMore]);// Todo loads more
  const handleLoadMore = () = > {
    if(! loadingRef.current && isMoreRef.current) {const temp = currentRef.current + 1;
      setCurrent(temp);
      runGetNoticeList({ current: temp, pageSize });
    };
  };
  useTouchBottom(handleLoadMore);

  return (
    <>{/* omit other code... * /}<Card tabList={[{ key:"',tab:'Notice'}]}>
        <Spin size="large" spinning={noticeListLoading} tip="Loading...">
          {list.length === 0 && <Empty />}

          {
            list.map(value => {
              return (
                <ArticleItem
                  key={value? .id}
                  item={value}
                />); })}<LoadMore status={loadMoreStatus} hidden={list.length= = =0} />
        </Spin>
      </Card>
    </>
  );
};

export default Notice;
Copy the code

conclusion

The code above is an implementation of bottom-loading in React and may not be the optimal solution. However, we use custom hooks in this case, which encapsulate loading more components and null-state components, and there are some other benefits as well. Only by accumulating various functional implementation schemes can we truly have the ability to independently develop large-scale projects. Only by continuous accumulation can we continue to grow!