React-table: Fully Controlled Component doesn't play nicely with manual/onFetchData?

Created on 25 May 2017  路  16Comments  路  Source: tannerlinsley/react-table

Problem Description

When managing some of the state outside ReactTable (for example using pageSize instead of defaultPageSize), it seems that when changing the page size the state passed into onFetchData contains the previous pageSize instead of the new one. This naturally causes the server to return unexpected data.

Is this in fact a bug or am I misunderstanding something about the fully controlled component or server-side data approaches?

I would say the onFetchData could use the local state value instead of the value from the state passed into onFetchData. But this is problematic when the component providing the onFetchData function is above the component managing the state, which I'm doing in order to provide a general ReactTable wrapper for consistency in my app.

Code Snippet

      <ReactTable
        data={data}
        columns={columns}
        manual
        onFetchData={this.props.onFetchData}     // note onFetchData function is in parent component
        pageSize={this.state.pageSize}
        onPageSizeChange={(pageSize, page) => this.setState({page, pageSize})}
      />

Steps to Reproduce

  1. Add breakpoints to onFetchData
  2. Click button to change page size
  3. Note state provided to onFetchData has old pageSize value

System Information

  • Chrome 57
  • OSX El Capitan
  • npm 3.10.10
  • yarn 0.21.3

Most helpful comment

I'll just post here in case other people come looking (not sure if this is how you did it though)....

  • you should set manual when using onFetchData because handling the other update callbacks (like onSortedChange will all cause an update if you pass their values back into ReactTable). onFetchData gets ALL the details of the changes (like the sorted, filtered and pagination details - store those in your state each time you do the backend call - you can use them again.
  • onFetchData still has to update state (setState) with the results of the backend call (or it calls something else that updates some global state machine like Redux or MobX
  • the state (usually called data) is passed as a prop to ReactTable - so when the onFetchData call has finished doing itself, ReactTable updates
  • if you split out the backend call logic into a reusable function you can call it from anywhere else in the the component - like a Refresh button or a Reload button
  • your function attached to your button (or whatever the action) can grab the state information (about sorting, filtering, pagination) and use that to call your reusable function - and can ALSO update the state (setState) that triggers off the update process again

This is just how React works - most React components don't allow you to change the state of children directly.

[I also gone to put this in to the FAQ in the Wiki]

All 16 comments

Interesting. My first thought would be to simply use your controlled state instead of the variables provided by onFetchData, but that may not be possible depending on some other assumptions:

  • Is onFetchData firing before or after onPageSizeChange?
  • Is your controlled pageSize state updated before or after onFetchData is called?

If your new controlled pageSize state is available when the onFetchData is called, I would just use your local state instead of the variable provided by onFetchData. If it's not updated, however, I think we need to look into fixing some of the sequencing here.

I'm having the same issue,

If you only use onPageChange to this.setState({page: pageIndex}), then this.state.page will be the old value when onFetchData fires, which will load the same data. So basically you have to click twice before the page actually changes.

I'm considering manually fetching data when onPageChange occurs, but then onFetchData will also happen and the data will fetch twice.

EDIT:

A simple solution is to wrap the contents of onFetchData with a setTimeout like so

    onFetchData={(state, instance) => {
      setTimeout(() => {
        fetchData(this.state.page+1, this.state.pageSize)
      }, 1)
    }}

this way the state will be accurate, but, you know...

Very interesting. I'll try for a fix on our next release. Thanks guys.

I'm having a similar issue. I'm using apollo-client to fetch data and the fetchMore function of Apollo to handle page change.

With server-side rendering the initial query is fired twice. One time by apollo and one time by react-table in lifecylcle.js.

    componentDidMount () {
      this.fireFetchData()
    }

Would also like to know if I'm going about this the wrong way or if this is a problem and there is a solution? :) Thanks!

EDIT:

By removing onFetchData and using async/await on setState functions the problem seems to be mitigated in most cases.

Only issue I still have is when navigating to last page.. so don't know if this is the most optimal solution.

  async pageChange(pageIndex) {
    const { limit } = this.state;

    await this.setState({
      offset: pageIndex * limit,
    });

   this.fetchData();
  }

  async pageSizeChange(pageSize) {
    await this.setState({
      limit: pageSize,
      offset: 0,
    });

   this.fetchData();
  }

  fetchData() {
    const { limit, offset } = this.state;
    this.props.loadMore({ limit, offset });
  }

EDIT 2:
Getting server side error because this.fetchData() gets invoked somewhere..

(node:30692) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ObservableQuery with this id doesn't exist: 14
(node:30692) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

EDIT 3:
The above edit #1 solved the original problem. Edit 2 is related to apollo-client and server-side rendering and react-table I'm currently only using react-table on client side until I can figure it out...

I managed to get this working with Apollo connected component and server side rendering now.

As above the problem was related to unhandled rejection of promise. Since react-table invokes this.fireFetchData() in componentDidMount() the fetch is fired twice.

The hacky solution is I set a state of initialLoaded: false and set this to true in componentWillRecieveProps().

For a future update of react-table it would be really nice if fireFetchData() wasn't invoked in componentDidMount() if using a fully controlled component (manual prop).

What do you think of this @tannerlinsley ? I can make a PR or new issue if you want to see it.

Thanks for a great component!

Full example if someone else uses Apollo and needs to solve this.

  constructor(props) {
    super(props);

    this.state = {
      initialLoaded: false,
    };

    this.sort = this.props.defaultSorted || [];
    this.limit = this.props.defaultPageSize || 25;

    this.pageChange = this.pageChange.bind(this);
    this.pageSizeChange = this.pageSizeChange.bind(this);
    this.sortChange = this.sortChange.bind(this);
  }

  componentDidMount() {
    if (!this.initialLoaded) {
      this.initialLoaded = true;
    }
  }

  pageChange(pageIndex) {
    this.offset = (pageIndex * this.limit);

    this.fetchData();
  }

  pageSizeChange(pageSize) {
    this.limit = pageSize;
    this.offset = 0;

    this.fetchData();
  }

  sortChange(sort) {
    this.sort = sort;

    this.fetchData();
  }

  fetchData() {
    const { initialLoaded } = this.state;

    if (this.initialLoaded && this.props.loadMore) {
      this.props.loadMore({
        sort: this.sort,
        limit: this.limit,
        offset: this.offset,
      });
    }
  }

  initialLoaded = false
  sort = []
  limit = 10
  offset = 0

  render() {
    ...
  }

So what would trigger the initial call of onFetchData?

Just to deal with the original issue of this post, I found a similar problem using manual / onFetchData / onSortedChange.

Indeed, the onSortedChange method is updating the component's state to hold the new "sorted" values, but onFetchData is called right away before the state is updated.

Using a setTimeout could work, but I preferred relying on this instead :

onFetchData(reactTableState) {
    this.setState({}, () => {
        // Here is the code to fetch asynchronously data from the server
    });

These hacks are ugly, and it would be better to have a cleaner solution. A callback "done" could be passed as a last argument of "onSortedChange" / "onPageSizeChange" / etc..., and people could use it as the second argument of "setState".

Does this issue fix @tannerlinsley ? I use the latest version 6.7.6 but still need to use

 onFetchData={(state, instance) => {
      setTimeout(() => {
        fetchData(this.state.page+1, this.state.pageSize)
      }, 1)
    }}

since it invoke onFetchData with old state without setTimeout

Any news on this? It a fix on the road map?

This is a very old thread. Firstly, onFetchData really should not be used with any of the other handlers (like onSortedChange or onFilteredChange etc.) as ALL the information passed in those handlers is provided to the onFetchData call when the user makes a change. If you use onFetchData with the other handlers, you are going to be causing race conditions.

My advice is that if you are using onFetchData that you MUST set manual to true and then handle ALL of the updates to your data through the onFetchData - otherwise there are conflicts with unpredictable results.

Thanks for the info! I use the other handlers ONLY for updating component state, not fetching data. Ill consider moving the state updating logic to onFetchData instead.

@gary-menzel
Thanks for this info! If we can't use the other handlers and onFetchData at the same time, do you know how we can trigger an OnFetchData call without the user interacting with the table? I was hoping to refresh the table when some state for another parent component changes....?
Any assistance greatly appreciated.

All good,I figured it out. Thx!

I'll just post here in case other people come looking (not sure if this is how you did it though)....

  • you should set manual when using onFetchData because handling the other update callbacks (like onSortedChange will all cause an update if you pass their values back into ReactTable). onFetchData gets ALL the details of the changes (like the sorted, filtered and pagination details - store those in your state each time you do the backend call - you can use them again.
  • onFetchData still has to update state (setState) with the results of the backend call (or it calls something else that updates some global state machine like Redux or MobX
  • the state (usually called data) is passed as a prop to ReactTable - so when the onFetchData call has finished doing itself, ReactTable updates
  • if you split out the backend call logic into a reusable function you can call it from anywhere else in the the component - like a Refresh button or a Reload button
  • your function attached to your button (or whatever the action) can grab the state information (about sorting, filtering, pagination) and use that to call your reusable function - and can ALSO update the state (setState) that triggers off the update process again

This is just how React works - most React components don't allow you to change the state of children directly.

[I also gone to put this in to the FAQ in the Wiki]

Hi @gary-menzel ,

First of all: sorry, I know it's an old thread, but the problem I'm facing is strongly related to this.

I want to render a filterable "server-side" ReactTable, with prefilled filtered prop (which comes from my app's state). The problem is that on the page, when editing the text in the filter's field, the onFetchData is called with the "old" filter value, hence I can't update the state to store the new filtered value(s).

Note that if the filtered prop is not passed to ReactTable, then everything works perfectly (onFetchData is called with the "new" value).

I've created a minimal showcase on codesandbox which you can find here . Just try to edit the search input and keep an eye on the JS console as well.

I've also tried using the handleFilteredChange to update the filters on my app's state, but that is called after onFetchData is called (and in short, that results in two requests to the server to get the data) and it's also not recommended as per your previous comment.

Please let me know if you have any updates and thank you in advance!

I'll just post here in case other people come looking (not sure if this is how you did it though)....

  • you should set manual when using onFetchData because handling the other update callbacks (like onSortedChange will all cause an update if you pass their values back into ReactTable). onFetchData gets ALL the details of the changes (like the sorted, filtered and pagination details - store those in your state each time you do the backend call - you can use them again.

Thanks for writing this, @gary-menzel

I am trying to use react table v6 and all the pagination and filtering will result in fetching from remote data source (I'm using Django).

Thanks to your reply here, i figured out how to do this.

If I want to write my experience using react table v6 so that all fetching are handled manually and sent to remote data. Can I contribute that to the wiki here? Or do I paste it here? What about a video walkthrough?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Abdul-Hameed001 picture Abdul-Hameed001  路  3Comments

DaveyEdwards picture DaveyEdwards  路  3Comments

LeonHex picture LeonHex  路  3Comments

pasichnyk picture pasichnyk  路  3Comments

monarajhans picture monarajhans  路  3Comments