next/link losing state when navigating to another page
Asked Answered
A

4

8

I am developing a library Next.js application. For the purposes of this question, I have two pages in my application: BooksPage which lists all books, and BookPage which renders details of a book. In terms of components, I have a <Books /> component which renders a <Book /> component for every book in my library database.

Here are my components:

Books.js:

function Books({ books }) {
  return (
    <>
      {books.map(book => (
        <Book key={book.id} book={book} />
      ))}
    </>
  );
}

Book.js:

class Book extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = { liked: false };
  }

  like = () => {
    this.setState({ liked: this.state.liked ? false : true })
  };

  render() {
    return (
      <>
        <Link href={`/books/${book.slug}`}>
          <a>{book.title}</a>
        </Link>

        <Button onClick={this.like}>
          <LikeIcon
            className={this.state.liked ? "text-success" : "text-dark"}
          />
        </Button>
      </>
    );
  }
}

Problem:

Say that I am on page BooksPage. When I click the like button of a <Book /> the icon color toggles properly in the frontend and the like is successfully added or removed in the backend. When I refresh BooksPage all the state is maintained and consistent.

The problem arises when I like something on BooksPage and then immediately navigate to BookPage without refreshing using next/link. There the like button is not toggled consistently and the state from BooksPage is lost. Notice that if I hard-refresh the page everything goes back to normal.

Slow solution: Do not use next/link.

Replace

<Link href={`/books/${book.slug}`}>
  <a>{book.title}</a>
</Link>

with

<a href={`/books/${book.slug}`}>{book.title}</a>

Fast solution: Keep using next/link?

Is there a way to use next/link and maintain state when navigating to another pre-rendered route?

Audit answered 1/9, 2020 at 13:7 Comment(2)
This is why REST uses DELETE method and HTTP_NO_CONTENT as response, so consumers will fetch affected object again from the detail view or do the math themselves. Please look into generics and don't implement half of the stuff that's there for you yourself.Stacy
And second, you don't need signals. You can just override Like.delete() and update refs before you call super().delete().Stacy
A
1

Pass the liked property of a book somehow through the API. Then, pass that prop down from the Books component to the Book component.

Add a componentDidUpdate() method to your book component.

class Book extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = { liked: this.props.liked };
  }

  componentDidUpdate(prevProps) {
    if (this.props.liked !== prevProps.liked) {
      this.setState({
        liked: this.props.liked,
      });
    }
  }

  like = () => {
    this.setState({ liked: !this.state.liked })
  };

  render() {
    return (
      <>
        <Link href={`/books/${book.slug}`}>
          <a>{book.title}</a>
        </Link>

        <Button onClick={this.like}>
          <LikeIcon
            className={this.state.liked ? "text-success" : "text-dark"}
          />
        </Button>
      </>
    );
  }
}
Audit answered 2/11, 2020 at 11:23 Comment(0)
C
4

TLDR: Any time the button needs to be changed, the API must change data, and it must update the closest parent's local state to update the button's appearance. The API will control all aspects of local state. You can't update local state unless an API request is successful. Therefore, the client and API are always 1:1.

The Button component in Book.js should NOT be maintaining its own state separately from the API data; instead, wherever you're fetching book data from the API, it should also be controlling the button's state (and its appearance). Why? Because with the current approach, the API request can fail, but the client will still update. As a result, the API and client may no longer be 1:1 (client shows liked, but API still shows that it's disliked/unliked).

In practice, you'll have a parent container that acts like a state manager. It fetches all relevant data, handles updates to the data, and displays the results using stateless child components. Any time a child component needs to be updated (such displaying a "like" or "dislike" button based upon a button press), it must first make an API request to change the data, then the API must respond with relevant data to the update the state used by the child:

enter image description here

Alternatively, if this component is reusable, then you'll conditionally render it using this.props.book (which comes from a parent container) or the child component must request data from an API to update its own local this.state.book. Same as the above diagram, the API requests control all aspects of state changes:

enter image description here

There's yet another approach that is the same as the diagram above, but it uses the child local state regardless of the parent's state. This child state will only be updated by its own class methods. This introduces a bit more complexity because you have to make sure that any parent state changes won't rerender the child component with stale data. Ultimately, which approach to take is up to you.

On a side note: This question doesn't make much contextual sense as libraries don't render pages nor do they attempt internal page navigations. Instead, they offer reusable components that can be utilized by one or many NextJS project pages or components.

Cappuccino answered 2/11, 2020 at 0:56 Comment(2)
So, in short, you are suggesting lifting up state to the <Books /> component and passing it as props to each of its child <Book /> components?Audit
When you're on the Books listing page, then yes, elevate the state. When you're on the individual book details page, then the state should reside within the Book component. But the most important aspect is that state is only changed when the API has been called and returns up-to-date data to update it. Any time a page is loaded, data is always refetched from the API; this way, the client and API are always 1:1.Cappuccino
H
1

You need to use Model.refresh_from_db(...)--(Django Doc) method to retrieve the updated value from the Database

class DeleteLikeView(APIView):

    def post(self, request, book):
        book = get_object_or_404(Book, id=book)
        print(book.num_of_likes)
        like = Like.objects.get(user=request.user, book=book)
        like.delete()

        book.refresh_from_db()  # here is the update
        
        print(book.num_of_likes)  # You will get the updated value
        return ...
Hamitic answered 29/10, 2020 at 15:15 Comment(0)
A
1

Pass the liked property of a book somehow through the API. Then, pass that prop down from the Books component to the Book component.

Add a componentDidUpdate() method to your book component.

class Book extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = { liked: this.props.liked };
  }

  componentDidUpdate(prevProps) {
    if (this.props.liked !== prevProps.liked) {
      this.setState({
        liked: this.props.liked,
      });
    }
  }

  like = () => {
    this.setState({ liked: !this.state.liked })
  };

  render() {
    return (
      <>
        <Link href={`/books/${book.slug}`}>
          <a>{book.title}</a>
        </Link>

        <Button onClick={this.like}>
          <LikeIcon
            className={this.state.liked ? "text-success" : "text-dark"}
          />
        </Button>
      </>
    );
  }
}
Audit answered 2/11, 2020 at 11:23 Comment(0)
C
0

In DeleteLikeView class you get book object. it retrieved from DB and saved in book variable. when you deleted like object num_of_likes attribute has been updated in DB but your variable still consists previous object. after like.delete() command you should get object again and print num_of_likes att. It is as your expected.

Christianly answered 29/10, 2020 at 13:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.