displaying search query results in all pages

Viewed 301

I've made an image search application using unsplash api. The app contains pagination and that's where the problem lies. When I search for something like, say flower, then it shows flowers in the first page but when I go to second page then there aren't flowers anymore but default photos which is generated by the fetchPhotos function in componentDidMount because of re-render I believe. Is there a way through which I can view the search query results in all the pages until I refresh the page?

sandbox

App.js

import React, { Component } from "react";
import Pagination from "./Pagination";
import List from "./List";
import "./styles.css";
import axios from "axios";

const LOAD_STATE = {
  SUCCESS: "SUCCESS",
  ERROR: "ERROR",
  LOADING: "LOADING"
};

const appId = "N1ZIgf1m1v9gZJhledpAOTXqS8HqL2DuiEyXZI9Uhsk";
const BASE_URL = "https://api.unsplash.com/photos?page=1";

export default class App extends Component {
  constructor() {
    super();
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      photos: [],
      totalPhotos: 0,
      perPage: 5,
      currentPage: 1,
      loadState: LOAD_STATE.LOADING,
      search: ""
    };
  }
  handleChange(e) {
    this.setState({ search: e.target.value });
  }

  handleSubmit() {
    const { search, perPage } = this.state;
    console.log(search);
    const url =
      "https://api.unsplash.com/search/photos?page=1&query=" +
      search +
      "&client_id=" +
      appId;

    const options = {
      params: {
        client_id: appId,
        page: 1,
        per_page: perPage,
        order_by: "popularity"
      }
    };

    this.setState({ loadState: LOAD_STATE.LOADING });
    axios
      .get(url, options)
      .then(response => {
        console.log(typeof response.data);
        let photos;
        try {
          photos = response.data.results;
        } catch {
          photos = [];
        }
        this.setState({
          photos,
          totalPhotos: parseInt(response.headers["x-total"]),
          currentPage: 1,
          loadState: LOAD_STATE.SUCCESS
        });
      })
      .catch(() => {
        this.setState({ loadState: LOAD_STATE.ERROR });
      });
  }

  componentDidMount() {
    this.fetchPhotos(this.state.currentPage);
  }

  fetchPhotos(page) {
    var self = this;
    const { perPage } = this.state;

    const options = {
      params: {
        client_id: appId,
        page: page,
        per_page: perPage,
        order_by: "popularity"
      }
    };

    this.setState({ loadState: LOAD_STATE.LOADING });
    axios
      .get(BASE_URL, options)
      .then(response => {
        self.setState({
          photos: response.data,
          totalPhotos: parseInt(response.headers["x-total"]),
          currentPage: page,
          loadState: LOAD_STATE.SUCCESS
        });
      })
      .catch(() => {
        this.setState({ loadState: LOAD_STATE.ERROR });
      });
  }

  render() {
    return (
      <div className="app">
        <input
          onChange={this.handleChange}
          type="text"
          name="search"
          placeholder="Enter query"
        />
        <button type="submit" onClick={this.handleSubmit} className="button">
          Submit
        </button>
        <Pagination
          current={this.state.currentPage}
          total={this.state.totalPhotos}
          perPage={this.state.perPage}
          onPageChanged={this.fetchPhotos.bind(this)}
        />
        {this.state.loadState === LOAD_STATE.LOADING ? (
          <div className="loader" />
        ) : (
          <List data={this.state.photos} />
        )}
      </div>
    );
  }
}

Pagination

import React, { Component } from "react";

export default class Pagination extends Component {
  pages() {
    var pages = [];
    for (var i = this.rangeStart(); i <= this.rangeEnd(); i++) {
      pages.push(i);
    }
    return pages;
  }

  rangeStart() {
    var start = this.props.current - this.props.pageRange;
    return start > 0 ? start : 1;
  }

  rangeEnd() {
    var end = this.props.current + this.props.pageRange;
    var totalPages = this.totalPages();
    return end < totalPages ? end : totalPages;
  }

  totalPages() {
    return Math.ceil(this.props.total / this.props.perPage);
  }

  nextPage() {
    return this.props.current + 1;
  }

  prevPage() {
    return this.props.current - 1;
  }

  hasFirst() {
    return this.rangeStart() !== 1;
  }

  hasLast() {
    return this.rangeEnd() < this.totalPages();
  }

  hasPrev() {
    return this.props.current > 1;
  }

  hasNext() {
    return this.props.current < this.totalPages();
  }

  changePage(page) {
    this.props.onPageChanged(page);
  }

  render() {
    return (
      <div className="pagination">
        <div className="pagination__left">
          <a
            href="#"
            className={!this.hasPrev() ? "hidden" : ""}
            onClick={e => this.changePage(this.prevPage())}
          >
            Prev
          </a>
        </div>

        <div className="pagination__mid">
          <ul>
            <li className={!this.hasFirst() ? "hidden" : ""}>
              <a href="#" onClick={e => this.changePage(1)}>
                1
              </a>
            </li>
            <li className={!this.hasFirst() ? "hidden" : ""}>...</li>
            {this.pages().map((page, index) => {
              return (
                <li key={index}>
                  <a
                    href="#"
                    onClick={e => this.changePage(page)}
                    className={this.props.current == page ? "current" : ""}
                  >
                    {page}
                  </a>
                </li>
              );
            })}
            <li className={!this.hasLast() ? "hidden" : ""}>...</li>
            <li className={!this.hasLast() ? "hidden" : ""}>
              <a href="#" onClick={e => this.changePage(this.totalPages())}>
                {this.totalPages()}
              </a>
            </li>
          </ul>
        </div>

        <div className="pagination__right">
          <a
            href="#"
            className={!this.hasNext() ? "hidden" : ""}
            onClick={e => this.changePage(this.nextPage())}
          >
            Next
          </a>
        </div>
      </div>
    );
  }
}

Pagination.defaultProps = {
  pageRange: 2
};

2 Answers

The issue is here:

onPageChanged={this.fetchPhotos.bind(this)}

because fetchPhotos doesn't use any sort of query to fetch a specific sort of photo, so when the page changes you get the default response.

You currently have two functions for fetching photos that are working differently. You need to refactor your code to a single photo-fetching function that uses the current query from the state.

Update

Something like this:

fetchPhotos(page=1) {
    var self = this;
    const { search, perPage } = this.state;
    const url = `https://api.unsplash.com/search/photos?page=${page}&client_id=${appId}`
    const url = search ? `${url}&query=search` : url

    const options = {
      params: {
        client_id: appId,
        page: page,
        per_page: perPage,
        order_by: "popularity"
      }
    };

    this.setState({ loadState: LOAD_STATE.LOADING });
    axios
      .get(url, options)
      .then(response => {
        self.setState({
          photos: response.data,
          totalPhotos: parseInt(response.headers["x-total"]),
          currentPage: page,
          loadState: LOAD_STATE.SUCCESS
        });
      })
      .catch(() => {
        this.setState({ loadState: LOAD_STATE.ERROR });
      });
  }

Then you can call fetchPhotos on mount and also use it instead of your onSubmit, which it looks like you can get rid of.

<button type="submit" onClick={this.fetchPhotos} className="button">
          Submit
        </button>

Your onPageChanged is given a function that fetches default photos. The only function you have that fetches photos based on dynamic input/state is in your handleSubmit. Extract the body of handleSubmit into an arrow function in another variable and call this new extracted function in handleSubmit at the same time pass this new function to onPageChanged prop. Leaving you with the same API calls in handleSubmit and onPageChanged

Since you are using a class component, extracting the body of handleSubmit into a constant may not be a good idea.

Instead rename your fetchPhotos method to fetchDefaultPhotos and your handleSubmit method to fetchPhotos to better represent what each method does.

Pass the newly renamed fetchPhotos to onPageChanged and call fetchDefaultPhotos in your componentDidMount. You may also create a new handleSubmit method that only makes a call to fetchPhotos, if still needed.

export default class App extends Component {
  constructor() {
    super();
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      photos: [],
      totalPhotos: 0,
      perPage: 5,
      currentPage: 1,
      loadState: LOAD_STATE.LOADING,
      search: ""
    };
  }

  handleChange(e) {
    this.setState({ search: e.target.value });
  }

  handleSubmit(newPage) {
    this.fetchPhotos(newPage)
  }

  fetchPhotos(newPage) {
    const { search, perPage } = this.state;
    console.log(search);
    const url =
      "https://api.unsplash.com/search/photos?page=1&query=" +
      search +
      "&client_id=" +
      appId;

    const options = {
      params: {
        client_id: appId,
        page: 1,
        per_page: perPage,
        order_by: "popularity"
      }
    };

    this.setState({ loadState: LOAD_STATE.LOADING });
    axios
      .get(url, options)
      .then(response => {
        console.log(typeof response.data);
        let photos;
        try {
          photos = response.data.results;
        } catch {
          photos = [];
        }
        this.setState({
          photos,
          totalPhotos: parseInt(response.headers["x-total"]),
          currentPage: newPage,
          loadState: LOAD_STATE.SUCCESS
        });
      })
      .catch(() => {
        this.setState({ loadState: LOAD_STATE.ERROR });
      });
  }

  componentDidMount() {
    this.fetchDefaultPhotos(this.state.currentPage);
  }

  fetchDefaultPhotos(page) {
    var self = this;
    const { perPage } = this.state;

    const options = {
      params: {
        client_id: appId,
        page: page,
        per_page: perPage,
        order_by: "popularity"
      }
    };

    this.setState({ loadState: LOAD_STATE.LOADING });
    axios
      .get(BASE_URL, options)
      .then(response => {
        self.setState({
          photos: response.data,
          totalPhotos: parseInt(response.headers["x-total"]),
          currentPage: page,
          loadState: LOAD_STATE.SUCCESS
        });
      })
      .catch(() => {
        this.setState({ loadState: LOAD_STATE.ERROR });
      });
  }
}

Update:

Here is a working sandbox: https://codesandbox.io/s/so-op-sbx-6rnhu

Related