I am trying to rewrite the following code here which was written in flow to a custom hook. I got stuck and the hook doesn't work as it should. Do you see any mistake I made?
The error I get is TypeError: elementRefs.ref is not a function
Here is how I call it:
parent.js
import MultiRef from "./MultiRef";
const [elementRefs] = useState(() => new MultiRef());
Here is my version:
function useMultiRef() {
const map = new Map();
const _refFns = new Map();
const ref = (key) => {
let refFn = _refFns.get(key);
if (!refFn) {
refFn = (value) => {
if (value == null) {
_refFns.delete(key);
map.delete(key);
} else {
map.set(key, value);
}
};
_refFns.set(key, refFn);
}
return refFn;
};
}
export default useMultiRef;
Here is the original code:
/* #flow */
type RefFn<V> = (value: V|null) => mixed;
export default class MultiRef<K,V> {
map: Map<K,V> = new Map();
_refFns: Map<K,RefFn<V>> = new Map();
ref(key: K): RefFn<V> {
let refFn: ?RefFn<V> = this._refFns.get(key);
if (!refFn) {
refFn = value => {
if (value == null) {
this._refFns.delete(key);
this.map.delete(key);
} else {
this.map.set(key, value);
}
};
this._refFns.set(key, refFn);
}
return refFn;
}
}
Update (parent.js):
/**
* Scrolls to element if var goToElement is set
*/
useEffect(() => {
const ref = elementRefs.map.get(goToElement);
// If no ref exists, scroll to top of the floor instead
if (!ref) {
floorRef.current.scrollTop = 0;
return;
}
// Scroll to element: scrollTo(x-coord, y-coord)
floorRef.current.scrollTo(0, ref.offsetTop);
// Wait for a short time to perform scrolling
const timer = setTimeout(() => {
// currentElementRef is needed for highlighting the element
setCurrentElementRef(ref);
}, 500);
return () => clearTimeout(timer);
}, [elementRefs, goToElement]);
You need to return ref value for elementRefs.ref to work, currently, you just run a void function.
Also, that's not a custom hook you implemented a simple function.
function multiRef() {
//...
const ref = (key) => {...};
return { ref };
}
Custom hook should use hooks, to get similar properties like a class, it should look something like:
function useMultiRef() {
const map = useRef(new Map());
const _refFns = useRef(new Map());
const ref = useCallback((key) => {
let refFn = _refFns.current.get(key);
if (!refFn) {
refFn = (value) => {
if (value == null) {
_refFns.current.delete(key);
map.current.delete(key);
} else {
map.current.set(key, value);
}
};
_refFns.current.set(key, refFn);
}
return refFn.current;
}, []);
return { ref, map };
}
export default useMultiRef;
Related
I'm trying to understand behavior of function based composition in JavaScript.
const Animal = (name) => {
let properties = { name };
return ({
get name() { return properties.name },
set name(newName) { properties.name = newName },
breathe: function() {console.log(`${this.name} breathes!`); }
})
}
const aquaticKind = (animal) => ({
swim: () => console.log(`${animal.name} swims`)
})
const walkingKind = (animal, noOfLegs) => {
const properties = { noOfLegs }
return ({
get noOfLegs() { return properties.noOfLegs },
set noOfLegs(n) { properties.noOfLegs = n; },
walk: () => console.log(`${animal.name} walks with ${properties.noOfLegs} legs`)
})
}
const egglayingKind = (animal) => ({
layEgg: () => console.log(`${animal.name} laid an egg`)
})
const Crocodile = (name) => {
const info = Animal(name);
return Object.assign(info,
walkingKind(info, 4),
aquaticKind(info),
egglayingKind(info)
);
}
const snooty = Crocodile('snooty');
snooty.breathe();
snooty.swim();
snooty.walk();
snooty.name = "coolie";
snooty.noOfLegs = 23 // I expected this to get update to 23
snooty.swim();
snooty.walk();
snooty.layEgg();
If you run the code above, you will see that noOfLegs never get updated, while name get updated. I can't seem to wrap my head around this. How do we make noOfLegs get updated too?
MDN Documentation for object.assign shows you how to copy "accessors"
Here's your code that works as expected - the completeAssign function is based entirely on the code in that link
const completeAssign = (target, ...sources) => {
sources.forEach(source => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
Object.getOwnPropertySymbols(source).forEach(sym => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
};
const Animal = (name) => {
const properties = { name };
return ({
get name() { return properties.name },
set name(newName) { properties.name = newName },
breathe () { console.log(`${this.name} breathes!`); }
})
}
const aquaticKind = (animal) => ({
swim: () => console.log(`${animal.name} swims`)
});
const walkingKind = (animal, noOfLegs) => {
const properties = { noOfLegs };
return ({
get noOfLegs() { return properties.noOfLegs },
set noOfLegs(n) { properties.noOfLegs = n; },
walk: () => console.log(`${animal.name} walks with ${properties.noOfLegs} legs`)
})
}
const egglayingKind = (animal) => ({
layEgg: () => console.log(`${animal.name} laid an egg`)
})
const Crocodile = (name) => {
const info = Animal(name);
return completeAssign(info,
walkingKind(info, 4),
aquaticKind(info),
egglayingKind(info)
);
}
const snooty = Crocodile('snooty');
snooty.breathe();
snooty.swim();
snooty.walk();
snooty.name = "coolie";
snooty.noOfLegs = 23;
snooty.swim();
snooty.walk();
snooty.layEgg();
as you see the code, on the handleUpdateFilter function the second "if" some how defaultCourseData is filtered as filteredData of the first "if". Thank you for helping me!
setup() {
const course = ref();
const defaultCourseData = null
const gettingCourse = async () => {
const { data } = await getCourse();
defaultCourseData = data
course.value = data;
};
const handleUpdateFilter = (data) => {
// data is filtering value
if (data.value.view) {
const filteredData = defaultCourseData.sort((a, b) => b.luotXem - a.luotXem);
course.value = filteredData;
}
if (!data.value.view) {
course.value = defaultCourseData // This case some how defaultCourseData filtered too
}
};
onMounted(() => {
gettingCourse();
});
return {
course,
handleUpdateFilter,
defaultCourseData
};
},
Your defaultCourseData variable isn't reactive.
Therefore it should be evaluated as null at every call.
Try this
defineComponent({
setup() {
const course = ref([]);
const defaultCourseData = ref([]);
const gettingCourse = async () => {
const { data } = await getCourse();
defaultCourseData.value = data
course.value = data;
};
const handleUpdateFilter = (data) => {
// data is filtering value
if (data.value.view) {
course.value = defaultCourseData.value.sort((a, b) => b.luotXem - a.luotXem);
}
if (!data.value.view) {
course.value = defaultCourseData.value // This case some how defaultCourseData filtered too
}
};
onMounted(async () => {
await gettingCourse();
});
return {
course,
handleUpdateFilter,
defaultCourseData
};
})
Edit: The actual issue here was, that the defaultCourseData always returned a sorted array as Array.prototype.sort() mutates the Array.
So making a copy solves the issue.
if (data.value.view) { course.value = [...defaultCourseData.value].sort((a, b) => b.luotXem - a.luotXem); }
I'm struggling a bit with unit testing the following:
const { element, state } = props;
const { theme } = state;
const { computedHeight, computedWidth } = theme;
const elementToSize = document.querySelector(element);
const observer = new ResizeObserver(element => {
const { contentRect } = element[0];
// set the viewport size in the state
dispatch(setViewportSize(state, contentRect.width, contentRect.height));
// perform the task function
wrapper(600, computedWidth, computedHeight, contentRect.width, contentRect.height).
then(() => {
elementToSize.style.height = `${contentRect.height <= computedHeight ? contentRect.height : computedHeight}px`;
elementToSize.style.width = `${contentRect.width <= computedWidth ? contentRect.width : computedWidth}px`;
}).catch( e => new Error(e));
});
observer.observe(document.documentElement);
const wrapper = async (ms, ...callbackArgs) => {
try {
await asyncInterval(checkSize, ms, ...callbackArgs);
} catch {
new Error('async Interval has failed...');
}
return await asyncInterval(checkSize, ms, ...callbackArgs);
};
};
export const SizeableEffect = (element, state) => [effectFn, { element, state } ];
In the code above, I have difficulties with unit testing the code inside the ResizeObserver.
I have the following in my test.
import ResizeObserver from 'resize-observer-polyfill';
import { SizeableEffect } from 'effects';
import { domMock } from '../mocks/dom';
jest.mock('resize-observer-polyfill');
describe('SizeableEffect', () => {
let stateMock, dispatch, effect, effectFn;
beforeEach(() => {
stateMock = {
viewportWidth: null,
viewportHeight: null,
theme: {
chatFrameSize: {
width: 300,
height: 500
}
}
};
dispatch = jest.fn();
effect = SizeableEffect('elementMock', stateMock);
effectFn = effect[0];
Object.defineProperties(window, {
document: {
value: domMock()
}
});
});
it('the effect should be called with the element', () => {
expect(effect[1]).toEqual(
expect.objectContaining({
element: 'elementMock',
state: stateMock
})
);
});
it('the effect function should perform the operations', () => {
effectFn(dispatch, { element: 'some', state: stateMock });
expect(document.querySelector).toHaveBeenCalledWith('some');
expect(dispatch).not.toHaveBeenCalled();
expect(ResizeObserver).toHaveBeenCalled();
});
});
And as you can see from the screenshot, I have uncovered lines where the ResizeObserver is doing the login by measuring the width' and height's against the viewport.
How can I cover those lines in the best way?
I have a problem with understanding how do async functions in js work. So I need to call async function for 5 times and measure the duration of every execution. In the end I should have an array with 5 time values.
I have smth like this
for (let i = 0; i < 5; i++) {
const run = async () => {
await test(thisTest, filename, unitTestDict);
};
measure(run).then(report => {
// inspect
const {tSyncOnly, tSyncAsync} = report;
// array.push(tSyncAsync)
console.log(`∑ = ${tSyncAsync}ms \t`);
}).catch(e => {
console.error(e)
});
}
Here I found the realization of time measure function:
class Stack {
constructor() {
this._array = []
}
push(x) {
return this._array.push(x)
}
peek() {
return this._array[this._array.length - 1]
}
pop() {
return this._array.pop()
}
get is_not_empty() {
return this._array.length > 0
}
}
class Timer {
constructor() {
this._records = new Map
/* of {start:number, end:number} */
}
starts(scope) {
const detail =
this._records.set(scope, {
start: this.timestamp(),
end: -1,
})
}
ends(scope) {
this._records.get(scope).end = this.timestamp()
}
timestamp() {
return performance.now()
}
timediff(t0, t1) {
return Math.abs(t0 - t1)
}
report(scopes, detail) {
let tSyncOnly = 0;
let tSyncAsync = 0;
for (const [scope, {start, end}] of this._records)
if (scopes.has(scope))
if (~end) {
tSyncOnly += end - start;
tSyncAsync += end - start;
const {type, offset} = detail.get(scope);
if (type === "Timeout")
tSyncAsync += offset;
}
return {tSyncOnly, tSyncAsync}
}
}
async function measure(asyncFn) {
const stack = new Stack;
const scopes = new Set;
const timer = new Timer;
const detail = new Map;
const hook = createHook({
init(scope, type, parent, resource) {
if (type === 'TIMERWRAP') return;
scopes.add(scope);
detail.set(scope, {
type: type,
offset: type === 'Timeout' ? resource._idleTimeout : 0
})
},
before(scope) {
if (stack.is_not_empty) timer.ends(stack.peek());
stack.push(scope);
timer.starts(scope)
},
after() {
timer.ends(stack.pop())
}
});
return await new Promise(r => {
hook.enable();
setTimeout(() => {
asyncFn()
.then(() => hook.disable())
.then(() => r(timer.report(scopes, detail)))
.catch(console.error)
}, 1)
})
}
I can call it inside the loop and get console.log with time for every iteration:
measure(run).then(report => {
// inspect
const {tSyncOnly, tSyncAsync} = report;
// array.push(tSyncAsync)
console.log(`∑ = ${tSyncAsync}ms \t`);
}).catch(e => {
console.error(e)
});
But when I try to push this console values to array, nothing comes out, the array remains empty. Is there a way to fill it in?
Is there a way to get rid of the this keyword on the line:
this.getOscillatorConfig(oscNumber);
below?:
const oscPlayer = (audioContext, voiceConfig) => ({
getOscillatorConfig(oscNumber)
{
return voiceConfig.oscillators[oscNumber];
},
getOscillator(oscNumber)
{
this.getOscillatorConfig(oscNumber);
let vco = audioContext.createOscillator();
vco.type = oscConfig.waveform;
return vco;
},
start: (vco, time, noteLength, frequency) => {
vco.frequency.value = frequency;
vco.start(time);
vco.stop(time + noteLength);
}
});
const octave = () => ({
applyPipeLength: (frequency, pipeLength) => {
return frequency / (parseInt(pipeLength, 10) / 8);
}
});
const Voice = (audioContext, voiceConfig) => {
return Object.assign(
{},
oscPlayer(audioContext, voiceConfig),
octave()
)
}
If I don't use it, I have getOscillatorConfig is undefined.
Or any other advice for how to structure this?
To be able to omit this, you have to create a function with name getOscillatorConfig that is available in the scope you want to call it:
const oscPlayer = (audioContext, voiceConfig) => {
function getOscillatorConfig(oscNumber) {
return voiceConfig.oscillators[oscNumber];
}
return {
getOscillator(oscNumber) {
getOscillatorConfig(oscNumber);
let vco = audioContext.createOscillator();
vco.type = oscConfig.waveform;
return vco;
},
start(vco, time, noteLength, frequency) {
// ...
}
};
};