Best practice for using scoped variables between imported functions

Viewed 177

I am trying to create an easing function on scroll but my main function is growing rather large and I want to be able to split it up. I am creating a requestAnimationFrame function that will ease the page scroll. The big issue I am having is that the render function with the animation frame will ease the Y value and then calls the update function to update the elements. But if I split this up and import them individually I am having trouble figuring out how to pass the updated values between functions. Many other functions also rely on these updated values.

I could take an object oriented approach and create a class or a constructor function and bind this to the function but it seems like bad practice to me:

import render from './render'
import update from './update'

const controller = () => {
  this.items = [];
  this.event = {
    delta: 0,
    y: 0
  }

  this.render = render.bind(this)
  this.update = update.bind(this)
  //ect
}

I could also break it up into classes that extend each other but I would like to take a more functional approach to the situation but I am having trouble figuring out how to achieve this.

Here is a very condensed version of what I am trying to do. Codesandbox.

const controller = (container) => {
  const items = [];
  let aF = null;

  const event = {
    delta: 0,
    y: 0
  };

  const render = () => {
    const diff = event.delta - event.y;
    if (Math.abs(diff) > 0.1) {
      event.y = lerp(event.y, event.delta, 0.06);
      aF = requestAnimationFrame(render);
    } else {
      stop();
    }
    update();
  };

  const start = () => {
    if (!aF) aF = requestAnimationFrame(render);
  };

  const stop = () => {
    event.y = event.delta;
    cancelAnimationFrame(aF);
    aF = null;
  };

  const update = () => {
    const y = event.y;
    container.style.transform = `translateY(-${y}px)`;
    items.forEach((item) => {
      item.style.transform = `translate(${y}px, ${y}px)`;
    });
  };

  const addItem = (item) => items.push(item);

  const removeItem = (item) => {
    const idx = items.indexOf(item);
    if (idx > -1) items.splice(idx, 1);
  };

  const onScroll = () => {
    event.delta = window.scrollY;
    start();
  };

  // and a bunch more stuff

  window.addEventListener("scroll", onScroll);

  return {
    addItem,
    removeItem
  };
};

export default controller;

I would like to be able to split this up and create a more functional approach with pure functions where I can import the update and render functions. The problem is that these functions are reliant on the updated global variables. Everywhere I look it says that global variables are a sin but I don't see a way to avoid them here. I have tried to look at the source code for some large frameworks to get some insight on how they structure their projects but it is too much for me to take in at the moment.

Any Help would be appreciated. Thanks.

2 Answers

What you wrote in the first example is completely valid, you basically did what classes are doing under the hood (classes are just syntactic sugar almost), but there are some problems with your code:

  • You use an arrow function, so this becomes window actually in that context.
  • You should use function expression instead, and initialize your instance using new, only in this case this will work how you want.

However, there are many solutions for Dependency Injection / Plugin systems which is what you're basically looking for, I'd advise you to look around this area.

In case you want a more functional approach, you need to avoid the this keyword and you need to use pure functions. I'd advice you not to share values in a context, but simply pass the necessary dependencies to your functions.

import render from './render'

const controller = () => {
    const items = []

    window.addEventListener('scroll', (event) => {
        start({ y: event.y })
    })
    
    const start = ({ y }) => {
        // Pass what render needs
        requestAnimationFrame(() => render({ container, items, y }))
    }
   
}

Using the above:

  • You don't need the update function in this file, render will import it on its own.
  • Your functions are pure, easily testable on their own.
  • You don't need to create an instance by using new (it's actually more expensive).
  • No need for shared state across functions.
  • No DI/Plugin solution need to implemented/used.

In case you do want to have shared state, you won't do that without classes and/or extra tooling around.

The way I'd do this is by creating a seperate .js file, then send the information you need into that js file right at the start of your main files code. Then just use the seperate file to store all your functions, then just call them with utils.myFunction(). At least that's how I do it in node.

Related