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.
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
I'll preface this question by saying I'm very new to the composition API so I'm still trying to figure out the best practices. I'm writing a useResizeObserver composable function using Vue's composition API in order to track dimension changes on a target element. Because I have to wait for the element to be mounted, I can't call my composable from top-level in the setup() method on my component as most examples show, but have to call it from onMounted() instead, where I have access to my element's ref.
onMounted(async () => {
elementDimensions.value = useResizeObserver(modalContent.value);
}
This forced me to have another ref that I named elementDimensions actually declared at the top level of the setup() function, which I initialise to null.
setup(props) {
const elementDimensions = ref(null);
const modalContent = ref(null);
...
}
Here is the useResizeObserver function itself:
export default function useResizeObserver(element) {
const elementHeight = ref(0);
const elementWidth = ref(0);
const resizeObserver = new ResizeObserver(entries => {
elementHeight.value = entries[0].contentRect.height;
elementWidth.value = entries[0].contentRect.width;
});
if (element) {
resizeObserver.observe(element);
}
onUnmounted(() => {
resizeObserver.disconnect();
});
return { elementHeight, elementWidth };
}
This actually works well to give me what I want but I'm wondering if there's a better way to achieve this. The way things are implemented right now, I end up with "nested" refs, since I end up wrapping elementHeight and elementWidth (which are already refs) into another ref inside the component (elementDimensions). Is there a better recommended way of doing this when you need to pass an element to a composable from onMounted()?
Composition functions are supposed to be used directly in setup, any other uses depend on the implementation and need to be verified.
Refs are basically objects that allow to pass a value by reference rather than by value. One of their uses is to pass a ref early and access when a value is up-to-date.
It should be:
setup(props) {
const modalContent = ref(null);
const elementDimensions = useResizeObserver(modalContent);
...
}
and
export default function useResizeObserver(element) {
const elementHeight = ref(0);
const elementWidth = ref(0);
let resizeObserver;
onMounted(() => {
if (element.value) {
resizeObserver = new ResizeObserver(...);
resizeObserver.observe(element.value);
}
}
onUnmounted(() => {
if (resizeObserver)
resizeObserver.disconnect();
});
return { elementHeight, elementWidth };
}
I'm trying to improve my JavaScript skills. I'm learning composability and functional patterns and I'm totally lost.
I have two functions: one mapping an array and the other called from within the previous function to generate the markup.
const names = ['peter', 'paul', 'patrice']
const namesMarkup = name => {
return `<p>${name}</p>`
}
const showNames = listOfNames => {
return listOfNames.map(el => {
return namesMarkup(el)
})
}
showNames(names)
I have been reading about HOF, which technically are functions that take a function as an argument and/or return a function.
How could I compose these functions to have a HOF?
I went through the basic examples like
const square = num => num * num
const plus10 = (num, callback) => {
return callback(num) + 10
}
console.log(addTwo(7, square))
but I cannot make my mind around the previous example and working with lists.
I will appreciate help since the more I research the more confused I get.
Your mistake is to assume an array for showNames. Never do this. Always implement the simplest version of a function. In FP array is a computational effect. Don't implement such an effectful function as default:
const nameMarkup = name => {
return `<p>${name}</p>`;
}
const nameMarkup2 = name => {
return `<p>${name.toUpperCase()}!</p>`;
}
const showName = f => name => {
const r = f(name);
/* do something useful with r */
return r;
}
const names = ['peter', 'paul', 'patrice']
console.log(
showName(nameMarkup) ("peter"));
// lift the HOF if you want to process a non-deterministic number of names:
console.log(
names.map(showName(nameMarkup2)));
Now swapping the markup just means to pass another function argument. Your showName is more general, because a HOF lets you pass part of the functionality.
If we drop the array requirement, your showNames doesn't do anything useful anymore. It still illustrates the underlying idea, though.
I am trying to update some elements on scroll by using animationFrame. I want to add an easing effect so I would like the elements to update their positions by the eased value. I figured the best way to do this would be to store all of the values in an array and update them accordingly. When each element is mounted I am sending them to a context element that adds them to the state value array.
My issue is that I cannot access the array from inside the animating function. It is available outside of the animating function but not inside. I am assuming that the animation is starting before the array is being populated but I have tried to stop and restart the animation when the blocks array changes with useEffect but to no avail.
Here is a codesandbox of the issue Example Of Issue
In the sandbox you can see in the animate() function in the ScrollContainer component I am console logging the blocks array and then after the function I am logging the same array. When you scroll the array does not log the available blocks only an empty array. But the available blocks are being logged correctly under this function.
const animate = () => {
const diff = yScroll - yCurrent;
const delta = Math.abs(diff) < 0.1 ? 0 : diff * ease;
if (delta) {
yCurrent += delta;
yCurrent = parseFloat(yCurrent.toFixed(2));
animationFrame = requestAnimationFrame(animate);
} else {
cancelAnimation();
}
console.log("Animating Blocks", blocks);
};
console.log("Available Blocks", blocks);
const addBlock = block => {
setBlocks(prev => {
return [...prev, block];
});
};
and here is how I am starting the animation
const startAnimation = () => {
if (!animationFrame) {
animationFrame = requestAnimationFrame(animate);
}
};
useEffect(() => startAnimation(), []);
Thanks.
I played with your example. It seems to me, that the problem is in the useEffect. If you add empty dependency to it, then it runs only once after the first render. There will be a second render when blocks state updates, but for the useEffect only the first state is visible because it runs only once and it uses the startAnimation with stale closure. This version of startAnimation uses the first version of the animation with the original state.
Your initial problem is solved if you add blocks to the useEffect as a dependency.
useEffect(() => {
yScroll = window.scrollY || window.pageYOffset;
yCurrent = yScroll;
startAnimation();
window.addEventListener("scroll", updateScroll);
return () => {
window.removeEventListener("scroll", updateScroll);
};
}, [blocks]);
I tried adding the animation, but is is quite choppy to me. I'm interested in your final solution. This is mime: https://codesandbox.io/s/scrolling-animation-frame-array-issue-d05wz
I use higher level animation libraries like react-spring. You can consider using something like this. I think it is much easier to use.
https://codesandbox.io/s/staging-water-sykyj
I encountered error in react wherein this is undefined. This is my first time developing a react application.
In UI, it says Unhandled Rejection (TypeError): Cannot read property 'setState' of undefined while in console the value of this is undefined.
Thank you for your help.
Here is the existing code:
import React, { useState, useEffect, useRef } from "react";
//import makeData from "../makeData";
import { useTableState } from "react-table";
import { Button } from "../Styles";
import Table from "../TransactionPanelTable";
// Simulate a server
const getServerData = async ({ filters, sortBy, pageSize, pageIndex }) => {
await new Promise(resolve => setTimeout(resolve, 500));
// Ideally, you would pass this info to the server, but we'll do it here for convenience
const filtersArr = Object.entries(filters);
// Get our base data
let rows = [];
for (let i = 0; i < 1000; i++) {
rows.push({
transaction_seq: 1234,
rec_count: 1234,
user_id: "test",
updated_at: "",
duration: 1.23
});
}
// Apply Filters
if (filtersArr.length) {
rows = rows.filter(row =>
filtersArr.every(([key, value]) => row[key].includes(value))
);
}
// Apply Sorting
if (sortBy.length) {
const [{ id, desc }] = sortBy;
rows = [...rows].sort(
(a, b) => (a[id] > b[id] ? 1 : a[id] === b[id] ? 0 : -1) * (desc ? -1 : 1)
);
}
// Get page counts
const pageCount = Math.ceil(rows.length / pageSize);
const rowStart = pageSize * pageIndex;
const rowEnd = rowStart + pageSize;
// Get the current page
rows = rows.slice(rowStart, rowEnd);
let checkedMap = new Map();
rows.forEach(row => checkedMap.set(row, false)); //preset each to false, ideally w/ a key instead of the entire row
this.setState({ checkedMap: checkedMap });
//handleCheckedChange(row) {
// let modifiedMap = this.state.checkedMap;
// modifiedMap.set(row, !this.state.checkedMap.get(row));
// this.setState({ checkedMap: modifiedMap });
//}
return {
rows,
pageCount
};
};
'this' is undefined because you haven't bounded the context of the component to the method.
As you are new to react, i would suggest you to go through concepts like bind,es6 and other js lingos so that you can code better and avoid such errors.
In this case you need to bind the context either by using bind method or by using es6 arrow functions which are class functions.
Add below code in constructor function where you are calling this getServerData function.
getServerData = getServerData.bind(this);
Here bind will return new method which will set context (this) to your calling class.
It is preferable to set state in class with in that promise resolved function.
First, let's answer your question. this is undefined because you are using ES6 modules and you are using an arrow function const getServerData = async (...) => {.
Inside arrow functions, the this binding is not dynamic, but is instead lexical. That means that this actually refers to the originating context (outside your arrow function).
In a basic js script, that would point to the Window global context. But you are using ES6 module, with import statements. One of the goals of ES6 modules is to isolate code. So inside such a module, there is no default access to a global Window context, so your this end up being undefined.
Now, several misconceptions are recognizable in your code.
writing an ES6 modules, you should have an export statement somewhere inside in order to consume it outside.
The request to a server should preferably be isolated in another function, so that the structure of your component will be more easy to apprehend, especially if you are learning React.
Read the very good React doc to understand the basics of it.
You should not use async when defining a React functional component
Since you have no state defined in your component (importing useState is not enough, you must use it: State Hooks doc), you cannot use setState.
this.setState refer to using React class syntax to define your component, useState refer to using State Hooks. You cannot do both in a same component. You have to choose.
So to sum-up, your getServerData function should be outside of your Component and should not have to do anything with state management. And you should build a React component that will call your function in a lifecycle method.
Stay confident, you will get it sooner than you think! ;-)
this.getServerData = this.getServerData.bind(this);
I wanted to know if it was possible by combining webpack and js' oop to arrive at a functional code like the one presented below.
The goal is to be able to isolate each of the elements of my site (sidebar, main,...) in different files while making sure that they can interact between.
Is this possible with webpack and pure js or not?
import ApplePicker from "./my_path/applePicker.js";
import NiceFarmer from "./my_path/niceFarmer.js";
const orchard = function () {
const appleNumber = 10;
const jack = new ApplePicker();
const daniel = new NiceFarmer();
jack.eatAnApple();
daniel.eatAnApple();
// appleNumber have to be now === 2
}
// Example of applePicker.js structure
const ApplePicker = function () {
this.eatAnApple = function () {
// Do something
}
}
export default ApplePicker;
yes you can.
If you simply want to consume a variable from applePicker.js:
// applePicker.js
export const apples = 10
If you want to be able to reassign that variable you might want to add a simple facade layer on top of that:
// applePicker.js
let apple = 5
export function getApple() {
return apple;
}
export function setApple(newValue) {
apple = newValue;
}
Even better, if you want other functions to "fire" when a variable is changed, use the Observer pattern