10

I have an Articles component which displays a list of posts. The list is paginated so that a maximum of 10 posts can display per page. There is a "Next Page" button that when clicked will update the component state and the corresponding url parameter like so:

page 1: /news

page 2: /news?page=2

page 3: /news?page=3

...and so on. The way the constructor() and render() methods are set up, it will read this URL parameter and display the correct posts if the user navigates directly to /news?page=3, for instance.

The issue I'm having is that the browser back and forward buttons don't seem to rerender the page. So if a user hits the "Next Page" button a few times and then hits the back button, the URL will update, but the page won't rerender. Is there a way to force it to do so?

I'm guessing there's a way to accomplish this by adding a window.history listener, but I wasn't sure if there was a recommended practice to go along with gatsby-link.

Here is a stripped down version of the component for reference:

import React, { Component } from 'react';
import { navigateTo } from 'gatsby-link';
import getUrlParameter from '../functions/getUrlParameter';

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

    /* external function, will grab value
    * of ?page= url parameter if it exists */
    const urlParamPage = getUrlParameter('page');
    const currentPage = urlParamPage ? urlParamPage : 1;

    this.state = {
      currentPage
    };
  }

  nextPage() {
    const { props, state } = this;

    const urlParam = state.currentPage > 1
      ? `?page=${state.currentPage}`
      : '';

    navigateTo(props.pathname + urlParam);
    this.setState({currentPage: this.state.currentPage + 1});
  }

  render() {
    const { props, state } = this;

    const articles = props.articles
      .slice(state.currentPage * 10, state.currentPage * 10 + 10);

    return (
      <div>
        <ul>
          {articles.map((article) => <li>{article.title}</li>)}
        </ul>

        <button onClick={() => this.nextPage()}>Next Page</button>
      </div>
    );
  }
}
dougmacklin
  • 2,560
  • 10
  • 42
  • 69

2 Answers2

9

Your page doesn't rerender because it is actually the same page - the component is already mounted. As you are fetching your data in constructor(), your page won't update because the constructor for a React component is called before it is mounted (source).

What you call urlParam are just a new prop that componentWillReceiveProps(nextProps) should receive in nextProps.location.search.

Edit:

You have to lift state up because only the root component will receive props.location on browser back and forward buttons. The pathname prop of your Articles component nerver changes, that is why componentWillReceiveProps never fires here.

The code:

/src/pages/root.js

import React, { Component } from 'react';
import { navigateTo } from 'gatsby-link';

import Articles from '../components/Articles';

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

    this.state = {
      currentPage: 1,
      data: {}, // your ext data
    };

    this.nextPage = this.nextPage.bind(this);
  }

  nextPage() {
    const { currentPage } = this.state;
    const { pathname } = this.props.location;
    const url = `${pathname}?page=${currentPage + 1}`;

    this.setState({ currentPage: currentPage + 1 });
    navigateTo(url);
  }

  componentWillReceiveProps(nextProps) {
    const { pathname, search } = nextProps.location;
    const getParam = /(\d+)(?!.*\d)/;

    const currentPage = search !== '' ? Number(search.match(getParam)[0]) : 1;

    /* get your ext. data here */
    const data = {};

    this.setState({ currentPage, data });
  }

  render() {
    const { currentPage, data } = this.state;
    return (
      <div>
        {/* other contents here */}
        <Articles
          nextPage={this.nextPage}
          currentPage={currentPage}
          data={data}
        />
      </div>
    );
  }
}

/src/components/Articles.js

import React from 'react';

const Articles = ({ nextPage, currentPage }) => {
  return (
    <div>
      <div>Page: {currentPage}</div>
      <button onClick={() => nextPage()}>Next Page</button>
    </div>
  );
};

export default Articles;
Nenu
  • 2,637
  • 1
  • 18
  • 24
  • Thank you for your reply @Nenu, however it seems that `componentWillReceiveProps` lifecycle method is never called, on initial page load or when navigating with the Next Page button – dougmacklin Mar 06 '18 at 19:00
  • This is strange. I found [here](https://stackoverflow.com/questions/40325715/componentwillreceiveprops-not-firing) that sometimes componentWillReceiveProps doesn't fire for obscure reason... I encourage you to create a new page in your gatsby project and see if componentWillReceiveProps works (this is what I did before answering thid code example). Then try debugging this one or refactor your code structure. – Nenu Mar 06 '18 at 20:12
  • I had the same intuition, sounds weird that it does not fire when your route change – Ben Mar 06 '18 at 21:32
  • Well within my gatsby project, a page imports and displays this component. My guess is that because it's not actually linking to a new page and instead updating the inner component's state and subsequently updating the URL, that is resulting in the back / forward buttons not having any effect. Is there a way I could make it load the whole page as if it were navigating from another? – dougmacklin Mar 07 '18 at 01:20
  • Oh I see. Well I think I got it. According to your code, you are passing a `pathname` prop to this component, which should be equal to `props.location.pathname` right ? If so, it is normal that `componentWillReceiveProps` isn't firing in your Articles component as `pathname` never changes. I would recommend you to lift states up. I edited my answer in this way. – Nenu Mar 07 '18 at 07:36
  • Hm I lifted the state but `componentWillReceiveProps` is still not firing for me when using the back / forward buttons. Is it working for you? – dougmacklin Mar 09 '18 at 21:25
  • @dougmacklin Yes it works well for me with the code above. I think we lack of information about your issue then... I think it will be easier if you can provide a [MVCE](https://stackoverflow.com/help/mcve) to test your code. – Nenu Mar 12 '18 at 09:18
  • 1
    @Nenu it turns out that in addition to lifting state, I also needed to update gatsby / gatsby-link! I was on versions 1.9.158 and 1.6.34 respectively and after updating to 1.9.231 and 1.6.39 it started working. thanks for all the help! – dougmacklin Mar 13 '18 at 00:22
2

This can be resolved using history object present inside window object.

Another way is using the React-Router-DOM, which is the latest routing module for routing in ReactJS Applications. So for reference to V4 router follow this code.

  import {BrowserRouter as Router, Route, Switch, Redirect} from 'react-router-dom';

After importing router, just try to scope your root Component inside it, then you can, create routes inside it.

  <Router>
    <div className="App" id="App">
      <Route path="/" exact component={Home}/>
      <Route path="/about" exact component={About}/>  
      <Route path="/misreports" exact component={MISReport}/>  
      <Route path="/contact" exact component={Contact}/>  
    </div>
  </Router> 

keep in mind that router should contain only single child other wise, it won't work as expected. This way routing becomes very easy to work with.

Amit Mundra
  • 166
  • 1
  • 5
  • Thank you for your reply @Amit, however I don't think the react router solution applies to this particular case. Could you maybe demonstrate the solution using the history object? – dougmacklin Mar 06 '18 at 19:02