Search code examples
javascriptreactjsflickr

React JS map array for infinite scroll


I have a simple React App, where I fetch the Flickr Public Feed API and display it. Unfortunately it is mapping the array several times, where I can see repeated photos. The request always returns an array with 20 items with the same pictures, explaining the repetition.

Check the code below:

import React, { Component } from 'react';
import $ from 'jquery';

import PhotoListItem from '../../components/photoListItem';
import Searchbar from '../../components/searchBar';
import ScrollButton from '../../components/scrollButton';

import '../app/index.css';

export default class PhotoApp extends Component {
    constructor(props) {
        super(props);

        this.state = {
            photoList: [],
            searchTerm: 'cyanotype',
            items: 10,
            loadingState: false,
        }
    }

    componentDidMount() {
        this.getPhotoList();
        this.onInfiniteScroll();
    }

    /* get data from Flickr public feed */
    getPhotoList = () => {
        const flickrApiPoint = "https://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?&tags=" + this.state.searchTerm;

        try {
            $.ajax({
                url: flickrApiPoint,
                dataType: 'jsonp',
                data: { format: "json" },
                success: function (data) {
                    this.setState({ photoList: data.items });
                }.bind(this)
            });
        }
        catch (err) {
            console.log(err);
        }
    }

    /* code for infinite scroll */
    onInfiniteScroll = () => {
        this.refs.iScroll.addEventListener("scroll", () => {
            if (this.refs.iScroll.scrollTop + this.refs.iScroll.clientHeight >= this.refs.iScroll.scrollHeight - 20) {
                this.loadMoreItems();
            }
        });
    }

    /*  */
    displayItems = () => {
        var items = [];
        for (var i = 0; i < this.state.items; i++) {
            items.push(
                this.state.photoList.map((photo, index) => {
                    const author = photo.author.split(/"/)[1];
                    const authorLink = photo.description.split(/"/)[1]
                    const description = photo.description.split(/"/)[13]
                    return (
                        <PhotoListItem
                            key={index}
                            url={photo.media.m}
                            photoLink={photo.link}
                            title={photo.title}
                            author={author}
                            authorLink={authorLink}
                            description={description}
                            tags={photo.tags} />
                    )
                })
            );
        }
        return items;
    }

    /*  */
    loadMoreItems = () => {
        if (this.state.loadingState) {
            return;
        }
        this.setState({ loadingState: true });
        setTimeout(() => {
            this.setState({ items: this.state.items + 10, loadingState: false });
        }, 1000);
    }

    render() {
        return (
            <div className='appContainer' ref="iScroll">
                <div className='appHeader'>
                    <h1 className='headerTitle'>Welcome to Flickr Alternative Photography Feed!</h1>
                </div>

                <div className='gridContainer'>
                    {this.displayItems()}
                </div>
                {this.state.loadingState ? <p className='loading'>Loading items...</p> : ""}
            </div>
        );
    }
}

HERE IS THE LIVE EXAMPLE

The problem is around this.displayItems(), but how can I fix this? Any help is appreciated. Thank you!


Solution

  • You can achieve this by slicing your array by the amount of items you want to show within your JSX :

    this.state.photoList.slice(0, this.state.items).map(
    

    You will then have to use the callback version of setState to use the old values of your state and increment what you want to show :

    this.setState(old => ({ items: old.items + 2, loadingState: false }));
    

    Fully functional example (using the "full page" option is recommended) :

    class PhotoListItem extends React.Component {
      render() {
        return (
          <div className="image-card">
            <img className="image-card__image" alt="" src={this.props.url} />
            <div className="image-card__body">
              <div className="image-title">
                <a href={this.props.photoLink}>{this.props.title}</a>
                <span className="image-author">
                  {" "}
                  by <a href={this.props.authorLink}>{this.props.author}</a>
                </span>
              </div>
              <div className="image-description">
                <span className="description">Description:</span>{" "}
                {this.props.description}
              </div>
              <div className="image-tags">
                <span className="tags">Tags:</span> {this.props.tags}
              </div>
            </div>
          </div>
        );
      }
    }
    
    class PhotoApp extends React.Component {
      constructor(props) {
        super(props);
    
        this.state = {
          photoList: [],
          items: 2,
          searchTerm: "cyanotype",
          loadingState: false
        };
      }
    
      componentDidMount() {
        this.getPhotoList();
        this.onInfiniteScroll();
      }
    
      /* get data from Flickr public feed */
      getPhotoList = () => {
        const flickrApiPoint =
          "https://api.flickr.com/services/feeds/photos_public.gne?jsoncallback=?&tags=" +
          this.state.searchTerm;
    
        try {
          $.ajax({
            url: flickrApiPoint,
            dataType: "jsonp",
            data: { format: "json" },
            success: function(data) {
              this.setState({ photoList: data.items });
            }.bind(this)
          });
        } catch (err) {
          console.log(err);
        }
      };
    
      /* code for infinite scroll */
      onInfiniteScroll = () => {
        this.refs.iScroll.addEventListener("scroll", () => {
          if (
            this.refs.iScroll.scrollTop + this.refs.iScroll.clientHeight >=
            this.refs.iScroll.scrollHeight - 20
          ) {
            this.loadMoreItems();
          }
        });
      };
    
      /*  */
      loadMoreItems = () => {
        if (this.state.loadingState) {
          return;
        }
        this.setState({ loadingState: true });
        setTimeout(() => {
          this.setState(old => ({ items: old.items + 2, loadingState: false }));
        }, 1000);
        this.getPhotoList();
      };
    
      render() {
        return (
          <div className="appContainer" ref="iScroll">
            <div className="appHeader">
              <h1 className="headerTitle">
                Welcome to Flickr Alternative Photography Feed!
              </h1>
            </div>
    
            <div className="gridContainer">
              {this.state.photoList.slice(0, this.state.items).map((photo, index) => {
                const author = photo.author.split(/"/)[1];
                const authorLink = photo.description.split(/"/)[1];
                const description = photo.description.split(/"/)[13];
                return (
                  <PhotoListItem
                    key={index}
                    url={photo.media.m}
                    photoLink={photo.link}
                    title={photo.title}
                    author={author}
                    authorLink={authorLink}
                    description={description}
                    tags={photo.tags}
                  />
                );
              })}
            </div>
            {this.state.loadingState ? (
              <p className="loading">Loading items...</p>
            ) : (
              ""
            )}
          </div>
        );
      }
    }
    
    ReactDOM.render(<PhotoApp />, document.getElementById("root"));
    body,
    html {
      margin: 0;
      min-height: 100%;
    }
    
    .appContainer {
      font-family: "Helvetica", sans-serif;
      width: 100%;
      height: 100vh;
      overflow: auto;
    }
    
    .appHeader {
      text-align: center;
      background-color: #033666;
      padding: 1rem;
    }
    
    .headerTitle {
      color: #fff;
    }
    
    .gridContainer {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      padding: 1rem;
      grid-gap: 1rem 1rem;
    }
    
    .loading {
      text-align: center;
      color: #033666;
    }
    
    @media only screen and (max-width: 320px) {
      .appHeader>h1 {
        font-size: 1.2rem;
      }
    }
    
    a,
    a:visited {
      color: #000;
      text-decoration: none;
    }
    
    a:hover {
      color: #033666;
      text-decoration: underline;
    }
    
    .image-card {
      display: flex;
      display: -webkit-box;
      display: -moz-box;
      display: -ms-flexbox;
      display: -webkit-flex;
      flex-direction: column;
      width: auto;
      height: auto;
      margin: .5rem;
      border-radius: 5px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, .15);
      background: #fff;
    }
    
    .image-card__image {
      border-radius: 5px 5px 0 0;
      width: 100%;
      height: 200px;
      object-fit: cover;
    }
    
    .image-card__body {
      padding: .5rem 1rem 1rem;
    }
    
    .image-title {
      font-weight: 600;
      margin: 0;
      word-wrap: break-word;
      padding-bottom: .7rem;
      cursor: pointer;
    }
    
    .image-author {
      font-weight: 100;
      font-size: .8rem;
      cursor: pointer;
    }
    
    .image-owner {
      margin-top: 0;
      font-size: .8rem;
    }
    
    .image-date-view-wrapper {
      display: flex;
      align-items: center;
      justify-content: space-between;
    }
    
    .image-description {
      padding-bottom: .7rem;
      font-size: .9rem;
      word-wrap: break-word;
    }
    
    .tags,
    .description {
      font-weight: 600;
    }
    
    .image-tags {
      font-size: .8rem;
      word-wrap: break-word;
    }
    
    .App {
      font-family: sans-serif;
      text-align: center;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>
    <div id="root" />