damon
damon

Reputation: 2897

How can I update the style of a styled-component based on scroll position without re-rendering an array of randomly placed sibling components?

I have a simple component which performs these actions:

  1. Using hooks, sets state to keep track of window.scrollY
  2. Attaches scroll eventListener to window to update state
  3. Creates an array of 10 styled-components - each with a randomly assigned absolute position
  4. return/renders the 10 dots and another styled-component - a triangle whose height is calculated using the scroll state defined in step 1.
const App = () => {
  // state to keep track of how many px scrolled
  const [scroll, setScroll] = useState(window.scrollY);
  const handleScroll = () => setScroll(window.scrollY);

  // set up listener on window to update scroll state on scroll
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
  }, []);

  // create 10 dots with random positions
  const dots = [];
  for (let x = 0; x < 10; x++) {
    dots.push(
      <Dot
        key={x}
        topValue={getRandomIntInclusive(1, 199)}
        leftValue={getRandomIntInclusive(1, 99)}
      />
    );
  }

  return (
    <div className="App">
      {dots}
      <Triangle heightValue={scroll / 10} />
    </div>
  );
};

What I want to happen

When I scroll down, the only thing I want to happen is the Triangle components height recalculates / re-renders. The originally rendered dots will not update or change.

My problem

As you might guess, the dots get re-rendered every scroll event, which is not only horrible for performance, but it causes the dots to have a new random position every single scroll event.

My questions

  1. How can I get the Triangle to re-render using the updated scroll value, but not have the Dot components re-render?
  2. More of a secondary question: If I also wanted to update some style in the Dot components on scroll (color, for example), but not update the random top and left positions that were created on the initial Mount, how would I do that?

I can accomplish something like this quite easily with vanilla JS, but I can't quite figure out the React way.

Complete working sandbox of this issue: https://codesandbox.io/embed/hopeful-greider-jb3td

Upvotes: 4

Views: 2289

Answers (1)

Shubham Khatri
Shubham Khatri

Reputation: 281726

What you need to do is calculate the dots position once and save it in an array so as to not recalculate them on each and every render when the scroll position changes

You can either use useEffect with empty deps array to do that like

// main component
const App = () => {
  // state to keep track of how many px scrolled
  const [scroll, setScroll] = useState(window.scrollY);
  const handleScroll = () => setScroll(window.scrollY);
  const [posArr, setPos] = useState([]);
  useEffect(() => {
    const pos = [];
    for (let x = 0; x < 10; x++) {
      pos.push({ topValue: getRandomIntInclusive(1, 199), leftValue: getRandomIntInclusive(1, 99)})
    }
    setPos(pos);
  }, [])
  // set up listener on window to update scroll state on scroll
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
  }, []);

  // create 10 dots with random positions
  const dots = [];
  for (let x = 0; x < 10; x++) {
    dots.push(
      <Dot
        key={x}
        topValue={posArr[x] && posArr[x].topValue}
        leftValue={posArr[x] && posArr[x].leftValue}
      />
    );
  }

  return (
    <div className="App">
      <br />
      <h2>Scroll to make triangle grow</h2>
      <br />
      <h4>Ideally, the dots would not rerender on scroll</h4>
      <Debug>Px scrolled: {scroll}</Debug>
      {dots}
      <Triangle heightValue={scroll / 10} />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Working demo

or initialise the state by using a callback function to useState

// main component
const App = () => {
  // state to keep track of how many px scrolled
  const [scroll, setScroll] = useState(window.scrollY);
  const handleScroll = () => setScroll(window.scrollY);

  // set up listener on window to update scroll state on scroll
  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
  }, []);

  const [posArr, setPos] = useState(() => {
    const pos = [];
    for (let x = 0; x < 10; x++) {
      pos.push({ topValue: getRandomIntInclusive(1, 199), leftValue: getRandomIntInclusive(1, 99) })
    }
    return pos;
  });
  // create 10 dots with random positions
  const dots = [];
  for (let x = 0; x < 10; x++) {
    dots.push(
      <Dot
        key={x}
        topValue={posArr[x].topValue}
        leftValue={posArr[x].leftValue}
      />
    );
  }

  return (
    <div className="App">
      <br />
      <h2>Scroll to make triangle grow</h2>
      <br />
      <h4>Ideally, the dots would not rerender on scroll</h4>
      <Debug>Px scrolled: {scroll}</Debug>
      {dots}
      <Triangle heightValue={scroll / 10} />
    </div>
  );
};

Working demo

Upvotes: 3

Related Questions