0

I want to make an API call that's based on the current state, but can't make the setState function work as an asynchronous function.

    handlePage = (direction: number) => {
    this.setState( async (state) => {
        const tickets: Ticket[] = await api.getTickets(`?page=${state.pageNum + direction}`)
        const pageNum: number = state.pageNum + direction;
        return { pageNum, tickets };
    });
}

Gets me the error:

Argument of type '(state: Readonly) => Promise<{ pageNum: number; tickets: Ticket[]; }>' is not assignable to parameter of type 'AppState | ((prevState: Readonly, props: Readonly<{}>) => AppState | Pick<AppState, "tickets" | "search" | "theme" | "pageNum"> | null) | Pick<...> | null'. Type '(state: Readonly) => Promise<{ pageNum: number; tickets: Ticket[]; }>' is not assignable to type '(prevState: Readonly, props: Readonly<{}>) => AppState | Pick<AppState, "tickets" | "search" | "theme" | "pageNum"> | null'. Type 'Promise<{ pageNum: number; tickets: Ticket[]; }>' is not assignable to type 'AppState | Pick<AppState, "tickets" | "search" | "theme" | "pageNum"> | null'. Type 'Promise<{ pageNum: number; tickets: Ticket[]; }>' is missing the following properties from type 'Pick<AppState, "tickets" | "search" | "theme" | "pageNum">': search, theme, pageNumts(2345)

It works if I fetch the data outside the setState method, but I'm afraid to make an API call to an outdated page number:

    handlePage = async (direction: number) => {
    const tickets: Ticket[] = await api.getTickets(`?page=${this.state.pageNum + direction}`)
    this.setState( (state) => {
        const pageNum: number = state.pageNum + direction;
        return { pageNum, tickets };
    });
}
6
  • 2
    By declaring the React state updater function async it then implicitly returns a Promise and that appears to not be compatible with your declared state type declaration. What do you mean by "outdated page number"? this.state.pageNum will be the value from the current render cycle when handlePage is invoked. Commented Mar 6, 2021 at 20:58
  • Seems like OP has presented an XY Problem Commented Mar 6, 2021 at 21:03
  • If this.state.pageNum isn't being used in rendering any part of the UI but being used only for API call, you can store it as ref using useRef hook. Commented Mar 6, 2021 at 21:10
  • What do you mean by outdated page number?? Commented Mar 6, 2021 at 21:19
  • By outdated page number I mean that if I will not set the state using the previous state, multiple quick clicks might failure to update the state as explained in react documentation Commented Mar 7, 2021 at 21:06

2 Answers 2

1

There are a lot of ways to handle this. I agree that this is an XY problem because my suggestion is to re-structure this so that you simply don't have this issue.

Recommendation: Key Tickets By Page

My best recommendation is that your state stores the tickets from all previously loaded pages in arrays keyed by the page number.

When the pageNum changes you (conditionally) fetch the results for that page. Each response knows where it belongs in the state so it doesn't matter when storing it whether this is still the current page or not. If pageNum changes twice in quick succession you would execute both calls and store both results but there won't be any confusion about which one is the correct result because each result is associated with its page number.

When it comes to accessing the tickets for the current page, just look up the tickets array for the current page number. It will either be an array Ticket[] or undefined if it is still loading. So that's extra information that you didn't have before because previously you couldn't know if the tickets that you were showing were the current ones or leftovers from the previous page.

Another benefit is that you don't need to repeat API calls if the user is going back and forth between pages. You can check if you already have data for that page before fetching.

import React from "react";

interface Ticket {
    // actual properties
}

interface State {
    tickets: Record<number, Ticket[] | undefined>;
    pageNum: number;
}

class MyComponent extends React.Component<{}, State> {
    state: State = {
        tickets: {},
        pageNum: 1,
    }

    handlePage = async (direction: number) => {
        const page = this.state.pageNum + direction;
        // can skip the API call if already loaded!
        if (this.state.tickets[page] !== undefined) {
            return;
        }
        const tickets: Ticket[] = await api.getTickets(`?page=${page}`);
        // we will use the prevState when saving the tickets
        this.setState(prevState => ({
            tickets: {
                ...prevState.tickets,
                [page]: tickets
            }
        }));
    }

    render() {
        // get the tickets for the current page
        const tickets = this.state.tickets[this.state.pageNum] ?? [];
        // might also want to distinguish between an empty result and an incomplete fetch
        const isLoading = this.state.tickets[this.state.pageNum] === undefined;

        return <div/>
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

My problem with this is scalability: I want my app to be able to work efficiently even if the DB stores a really big number of tickets, therefore sending the whole DB from the API to each client regardless of the wanted page number would be wasteful.
Perhaps I worded it improperly. This code does not preload pages. You don't load each page until you need it, but once you've loaded that page you hold onto it indefinitely.
But you haven't actually included where in your code you call handlePage so I guess technically it doesn't do anything right now.
I understand what you meant now, it makes much more sense :-) Thank you.
0

setState does only that job sets the state (and triggers a re render :P); I think you can change your function to this:

handlePage = async (direction: number) => {
    const tickets: Ticket[] = await api.getTickets(`?page=${this.state.pageNum + direction}`);
    
    this.setState({
        pageNum: this.state.pageNum + direction,
        tickets
    })
}

2 Comments

Maybe I wasn't clear enough but the reason I wanted to set the state based on the previous state is to avoid updating failure as explained in react's documentation. So I'm afraid that using your suggestion, the page number will not update as it works exactly like a counter.
Any time your next state depends on the previous state, i.e. like added a direction to current state, you absolutely should use a functional state update. This answer will only work most of the time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.