Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

next/link losing state when navigating to another page

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?

like image 696
fredperk Avatar asked Sep 01 '20 13:09

fredperk


3 Answers

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 ...
like image 89
JPG Avatar answered Oct 23 '22 22:10

JPG


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>
      </>
    );
  }
}
like image 1
fredperk Avatar answered Oct 23 '22 23:10

fredperk


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.

like image 1
Matt Carlotta Avatar answered Oct 23 '22 22:10

Matt Carlotta