4

I have a ShowcaseMovie component which fetches data on componentDidMount() and sets it to state. The component renders Card components to display the data as well as four button elements upcoming, top_rated, popular and now_playing which allow the user to toggle between the relevant data. Each button has an onClick event which calls changeFilter and sets state currentFilter to the selected key.

The problem: When the filter buttons are clicked, sometimes the page will jump to the top (if it's not already there). I've tried to find solutions to this but I can't seem to understand what is happening. Any suggestions will be a great help, thank you in advance.

Update: This issue seems to happen when there is no height set to an element with dynamic children. If I set the height on ShowcaseMovie to something large like height: 200vh it goes away.

I believe I've solved my problem but would love to hear other thoughts as to why this happens and some other ways to fix it. It's difficult to set a height to a parent when you don't know how much content is going to be rendered (or the height of that content). min-height would help but still kind of a quick fix.

ShowcaseMovie.js

import React, { Component } from "react";
import Card from "./Card";
import "../css/ShowcaseMovie.css";
import { v4 as uuidv4 } from "uuid";
import { formatString, buildMovieState } from "../utilities";

class ShowcaseMovie extends Component {
  static defaultProps = {
    filterNames: ["upcoming", "popular", "now_playing", "top_rated"]
  };

  constructor(props) {
    super(props);
    this.state = {
      upcoming: [],
      now_playing: [],
      popular: [],
      top_rated: [],
      currentFilter: this.props.filterNames[0]
    };
  }

  changeFilter = e => {
    e.preventDefault();
    const type = e.target.name;
    this.setState({ currentFilter: type });
  };

  componentDidMount() {
    this.props.filterNames.map(name => this.fetchMovies(name));
    // setInterval(() => {
    //   this.timeoutFilter();
    // }, 10000);
  }

  async fetchMovies(type) {
    try {
      const res = await fetch(
        `url`
      );
      const data = await res.json();
      if (data) {
        this.setState(state => ({
          ...state,
          [type]: buildMovieState(data)
        }));
      }
    } catch (error) {
      console.log(error);
    }
  }

  render() {
    const { currentFilter } = this.state;
    const movies = this.state[currentFilter].map((movie, i) => (
      <Card key={uuidv4()} movie={movie} index={i} />
    ));
    const buttons = this.props.filterNames.map(name => (
      <button
        type="button"
        key={name}
        name={name}
        className={`ShowcaseMovie-btn ${
          currentFilter === name ? "active" : ""
        }`}
        disabled={currentFilter === name}
        onClick={this.changeFilter}>
        {formatString(name)}
      </button>
    ));

    return (
      <section className="ShowcaseMovie">
        <div className="ShowcaseMovie-container">
          <h2 className="ShowcaseMovie-header">Movies</h2>
          <div className="ShowcaseMovie-btn-container">{buttons}</div>
        </div>
        <div className="ShowcaseMovie-grid">{movies}</div>
      </section>
    );
  }
}

ShowcaseMovie.css

.ShowcaseMovie {
  padding: 4rem 10%;
}

.ShowcaseMovie-container {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
}

.ShowcaseMovie-button-container {
  display: flex;
  justify-content: center;
  align-items: center;
}

.ShowcaseMovie-container::after {
  content: "";
  background-color: #64b5f6;
  height: 80%;
  width: 6px;
  left: 0;
  position: absolute;
  border-radius: 1px;
}

.ShowcaseMovie-header {
  font-size: 3rem;
  font-weight: 200;
  margin: 0 5rem;
}

.ShowcaseMovie-btn {
  outline: none;
  border: none;
  background-color: transparent;
  font-size: 1.6rem;
  font-weight: 500;
  letter-spacing: 1px;
  padding: 1rem;
  margin-left: 4rem;
  color: white;
  opacity: 0.5;
  cursor: pointer;
  transition-property: opacity;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.ShowcaseMovie-btn:hover {
  opacity: 1;
  transition-property: opacity;
  transition-duration: 300ms;
  transition-timing-function: ease;
}

.ShowcaseMovie-btn.active {
  opacity: 1;
  cursor: auto;
  color: #64b5f6;
}

.ShowcaseMovie-grid {
  display: grid;
  gap: 3rem;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

Card.js

import React, { Component } from "react";
import "../css/Card.css";

class Card extends Component {
  render() {
    const { title, poster_path } = this.props.movie;
    const style = { animationDelay: `${80 * this.props.index}ms` };
    return (
      <div className="Card">
        <div className="Card-inner" style={style}>
          <img
            src={`https://image.tmdb.org/t/p/w500/${poster_path}`}
            alt=""
            className="Card-img"
          />
          <p className="Card-name">{title}</p>
        </div>
      </div>
    );
  }
}

export default Card;

Card.css

.Card {
  display: block;
  transition: transform 300ms ease;
}

.Card:hover {
  transform: translateY(-5px);
  transition: transform 300ms ease;
}

.Card-inner {
  position: relative;
  display: block;
  cursor: pointer;
  height: 100%;
  opacity: 0;
  animation-name: moveUp;
  animation-duration: 500ms;
  animation-delay: 50ms;
  animation-timing-function: ease;
  animation-fill-mode: forwards;
}

.Card-inner::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: linear-gradient(transparent, rgba(33, 47, 61, 0.8));
  border-bottom-left-radius: 2px;
  border-bottom-right-radius: 2px;
  z-index: 100;
  opacity: 1;
  transition: opacity 300ms ease;
}

.Card-inner:hover::after {
  opacity: 0;
  transition: opacity 300ms ease;
}

.Card-inner::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image: linear-gradient(transparent, rgba(100, 180, 246, 0.6));
  border-bottom-left-radius: 2px;
  border-bottom-right-radius: 2px;
  z-index: 100;
  opacity: 0;
  transition: opacity 300ms ease;
}

.Card-inner:hover::before {
  opacity: 1;
  transition: opacity 300ms ease;
}

.Card-img {
  display: block;
  position: relative;
  object-fit: cover;
  max-width: 100%;
  min-height: 100%;
  z-index: 0;
  border-radius: 2px;
}

.Card-name {
  position: absolute;
  bottom: 0;
  left: 0;
  margin: 0 2rem 2rem 2rem;
  z-index: 150;
  font-weight: 400;
  text-transform: uppercase;
  font-size: 1.4rem;
  letter-spacing: 2px;
}

@keyframes moveUp {
  0% {
    transform: translateY(5rem);
  }

  100% {
    transform: translateY(0);
    opacity: 1;
  }
}

utilities.js

export const formatString = name => {
  return name
    .replace("_", " ")
    .split(" ")
    .map(w => w[0].toUpperCase() + w.slice(1))
    .join(" ");
};

export const buildMovieState = data => {
  if (data.results) {
    const movies = data.results.filter(
      d => d.backdrop_path && d.id && d.title && d.poster_path
    );
    return movies.length > 10 ? movies.slice(0, 10) : movies;
  } else {
    return [];
  }
};

Kyle Lambert
  • 369
  • 2
  • 13
  • Please include the Card component source too. – Leon Vuković Mar 25 '20 at 09:44
  • Hi Leon, I have just added the Card component source. Thanks – Kyle Lambert Mar 25 '20 at 09:52
  • For such cases that have UI issues, leaving a CodeSandBox project is a very good solution. including the codes just make the solution harder than before. – AmerllicA Mar 25 '20 at 10:22
  • I tried creating a codesandbox that uses your code. I added some fake data and do see the page "jump up" when clicking on buttons. But that's the way the browser reacts: it only happens when the new view has less height than the previous and keeping the same scroll is not possible since there's not the same "depth" to the page. Is this the issue you're talking about? https://codesandbox.io/s/affectionate-zhukovsky-fonbc – Nicolas SEPTIER Mar 25 '20 at 10:31
  • I've just created a CodeSandbox then, I've added a spacer at the top of the page to empathise the issue. If you scroll down to the ShowcaseMovie component (so the white spacer isn't showing) then click between the filter buttons you find that it will jump to the top of the page. This happens every 10 or so clicks. – Kyle Lambert Mar 25 '20 at 11:12
  • Thanks for the reply Nicolas I think you're on the right track but my demo appear to jump right to the top of the page. So if I have more sections above this component, it will jump past those and to the very top. – Kyle Lambert Mar 25 '20 at 11:17
  • @KyleLambert do you have any errors in the console? – Leon Vuković Mar 25 '20 at 11:45
  • No errors in the console, it seems to go away it if I set a height on the ShowcaseMovie itself. ```.ShowcaseMovie { height: 200vh } ``` Any ideas as to why this is? Thanks – Kyle Lambert Mar 25 '20 at 12:00

2 Answers2

1

I was able to stop this from happening by returning false from the onClick call. Like this:

onClick={
   doSomething()
   return false
}
Davis Jones
  • 1,504
  • 3
  • 17
  • 25
0

The page jumping could be from components unnecessarily re-rendering. Try wrapping all your components with React.memo( component name)

Embedded_Mugs
  • 2,132
  • 3
  • 21
  • 33