Can't understand why async/await isn't working NextJs - async-await

I'm looking for a help with how to make a chunk load, when the user scrollbar, and reach a specific div, then it shoul run a function one time, but the code run multiples time:
async function loadMore(){
console.log('i load more');
}
window.addEventListener('scroll', async (event) => {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 1 ) {
document.getElementById('final').style.height = '3000px'
let msg = await loadMore()
console.log('i finish')
document.getElementById('final').style.height = '30px'
}
}, {
passive: true
});
return (
<div id='final'>
<Image src="/ajax-loader.gif" width={60} height={60} alt="loader spinner"></Image>
</div>
)

Few things:
You should add the window event listener in a useEffect.
If you add the event listener in the render phase of the component, it will run add a event listener on every render which might be 1 of the reasons why it is running multiple times.
You also need to cleanup the event listener as a cleanup of useEffect else you will again end up with a lot of event listeners
You need to debounce the onScroll handler
Scroll event is triggered several times when you scroll which is something you might not want. So, adding a debounce will help reduce the number of times, the handler is called when a user scrolls.
Lodash's debounce is 1 popular implementation of debounce. You can choose other implementations if you want or create your own one.
import debounce from "lodash.debounce";
function YourComponent() {
async function loadMore(){
console.log('i load more');
}
// "useEffect" so that you don't add a new event listener
// on every render
useEffect(() => {
const onScroll = async (event) => {
// Whatever you want to do when user scrolls
}
// This is the debounced "onScroll" function instance
// "500" specifies that it will be debounced for 500 milliseconds
const debouncedOnScroll = debounce(onScroll, 500);
// Attach the event listener to window
window.addEventListener('scroll', debouncedOnScroll);
// Cleanup the event listener when component unmounts or
// when the "useEffect" runs again.
return () => window.removeEventListener('scroll', debouncedOnScroll);
}, []);
return (
<div id='final'>
{/* Rest of your JSX */}
</div>
)
}

trigger window event listener inside useEffect
make sure to cleanup the event: return () => window.removeEventListener('scroll', callback);

Related

Jest store state and RTL rendered component's onClick event handler different states

I'm using the following code to test a state-dependent react component using jest and rtl:
test("render author, date and image correctly after going next post", async () => {
const store = configureStore({
reducer: {
data: dataReducer
}
});
const Wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
);
render(<Post />, { wrapper: Wrapper });
const getSpy = jest.spyOn(axios, 'get').mockReturnValue(mockPostJSON);
await store.dispatch(FETCH_POSTS());
expect(getSpy).toBeCalledWith('https://www.reddit.com/r/EarthPorn/.json');
const beforeClick = await screen.findByTestId('authorAndDate');
expect(beforeClick.innerHTML.toString()).toBe(mockPostsList[0].author + ' - ' + mockPostsList[0].date);
fireEvent.click(screen.getByText('Next post'));
const afterClick = await screen.findByTestId('authorAndDate');
expect(afterClick.innerHTML.toString()).toBe(mockPostsList[1].author + ' - ' + mockPostsList[1].date);
})
The problem I'm having is that before the click everything in the store is set up correctly and the authorAndDate element displays the first item in the array of posts. But after the click is fired the store goes back to the initial state it had before loading the mock data. I checked within the component's event handler and right before it does anything the state has been reset. The code is as follows:
const handleNextClick = () => {
store.dispatch(GO_NEXT_POST());
store.dispatch(FETCH_COMMENTS());
}
I've been an hour over the code trying to find something that would reset the state and found nothing. I'm guessing it's some kind of interaction between jest and rtl but I can't figure out why the store in the test has one state and the store in the component's event handler has another :S
Well, figured it out. Can't use store.dispatch directly as it's accessing a stale state. Needed to use the useDispatch hook. Hope this serves anybody who faces the same problem in the future.

rxjs poll for data on timer and reset timerwhen manually refreshed

I am using the following libraries in the relevant application: Angular 4.x, ngrx 4.x, rxjs 5.4.x
I have an api that I need to poll every 5 minutes. The user is also able to manually refresh the data. That data is stored in an ngrx store. I am using ngrx effects so the data is retrieved by dispatching an action of type 'FETCH'.
I want to setup a rxjs stream where it will dispatch the 'FETCH' action to the ngrx store. It will be a sliding 5 minute timer that resets when the user manually updates the store. The stream should initially emit a value when subscribed.
I'm not sure how I can reset the timer. In plain javascript I would do something like the following:
console.clear();
let timer;
let counter = 0;
function fetch() {
console.log('fetch', counter++);
poll();
}
function poll() {
if (timer != null) {
window.clearTimeout(timer);
}
timer = window.setTimeout(() => {
console.log('poll');
fetch();
}, 5000);
}
function manualGet() {
console.log('manual');
fetch();
}
fetch();
<button onClick="manualGet()">Get Data</button>
Question: How do I emit on an interval that is reset when another stream emits like the example again?
You want two components to your stream – a timer and some user input. So let's start with the user input. I'll assume some button which can be clicked:
const userInput$ = Observable.fromEvent(button, 'click');
Now we want to start a timer which resets everytime userInput$ emits. We can do that using
userInput$.switchMap(() => Observable.timer(0, 5000));
However, we also want this stream to start without the user having to first click the button. But that's also not a problem:
userInput$.startWith(null);
Now we put it all together:
Observable.fromEvent(button, 'click')
.startWith(null)
.switchMap(() => Observable.timer(0, 5000))
.subscribe(() => dispatchFetch());
Note that I am following your examples of using a 5 second timer, not a 5 minute timer (which you mentioned in the question.)
After writing it out in vanilla JS I realized that the source of the timer should be the data. I was struggling to figure out what the source would be. Clearly it couldn't be the timer since I needed to reset it.
I'm open to better options but here is how I solved it:
console.clear();
let counter = 0;
const data = new Rx.BehaviorSubject(null);
function fetch() {
data.next(counter++);
}
function manualGet() {
console.log('manual');
fetch();
}
// setup poll
data.switchMap(() => Rx.Observable.timer(5000))
.subscribe(() => {
console.log('poll');
fetch();
});
// subscribe to the data
data.filter(x => x != null).
subscribe(x => { console.log('data', x); });
// do the first fetch
fetch();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
<button onClick="manualGet()">Get Data</button>
With ngrx I am listening for the success action related to the fetch event.

NavigationExperimental replace scene with animation

Its way to replace last scene in stack with newone? Like new scene is pushed with animation and older is silently poped from stack when push animation end. NavigationExperimental StateUtils replaceAt and replaceAtIndex only change scene on top without animation.
There is no utility function in NavigationStateUtils that does this for you but what you have to do is push and then at the very end of the navigation transition animation you do a reset with all the routes except the route before the newest one.
Since you're using NavigationCardStack, you have to do the reset on the component that you're pushing using InteractionManager because NavigationCardStack does not have a callback prop to call when the transition is finished.
Here's an example:
// Navigation reducer
function routeReducer(
navigationState = {
routes: [],
index: 0,
},
action,
) {
switch (action.type) {
case 'replaceWithPushAnimation':
// Pass a `reset` flag to your component so it knows to `resetWithoutRoute`
return NavigationStateUtils.push(navigationState, action.route);
case 'resetWithoutRoute':
return NavigationStateUtils.reset(
navigationState,
[
// Copy of all the routes except for navigationState.routes[length - 2]
]);
default:
return navigationState;
}
}
// The component that you pushed
class PushedComponent extends React.Component {
componentDidMount() {
if (this.props.shouldResetWithoutPrevious) {
// This runs after the navigation transition is over
InteractionManager.runAfterInteractions(() => {
// This function calls the reducer to trigger the
// routes reset
this.props.onNavigate({
type: 'resetWithoutRoute',
});
});
}
}
// render() {}
}
If you don't like this approach, you can use NavigationTransitioner, which has a onTransitionEnd callback prop to do the reset, however, because it's a lower-level API, you have to implement the navigation transitions yourself.

How I can detect window resize instantly in angular 2?

Some features in my component turn on or off depend on browser size, therefore I want to check browser width on resize event. However, I could do it using OnInit method. But I need to refresh browser when resize happened to update browser width
ngOnInit() {
if (window.innerWidth <= 767){
---- do something
}
}
I tried to use OnChanges method, but it does not work either.
OnChanges(changes:SimpleChanges){
console.log( 'width:====>' + changes[window.innerWidth].currentValue);
if ( changes[window.innerWidth].currentValue <= 767 ){
---- do something
}
}
is there any suggestions or alternative way to accomplish this?
You could just put handler on resize event over window object, but this will allow you to put only single resize event, latest registered event on onresize will work.
constructor(private ngZone:NgZone) {
window.onresize = (e) =>
{
//ngZone.run will help to run change detection
this.ngZone.run(() => {
console.log("Width: " + window.innerWidth);
console.log("Height: " + window.innerHeight);
});
};
}
To make it more angular way use #HostListener('window:resize') inside your component, which will allow to call your resize function(on which HostListner decorator has been mount) on resize of window.
#HostListener('window:resize', ['$event'])
onResize(event){
console.log("Width: " + event.target.innerWidth);
}
Use HostListener. You should probably debounce the resize event though before doing anything, it will fire everytime the size changes which could be dozens or hundreds of times in a few milliseconds as the user drags the window size.
import { Component, HostListener } from '#angular/core';
#Component({...})
class TestComponent {
#HostListener('window:resize')
onWindowResize() {
//debounce resize, wait for resize to finish before doing stuff
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
}
this.resizeTimeout = setTimeout((() => {
console.log('Resize complete');
}).bind(this), 500);
}
}
An easier way would be using the resize method on the html block that you want to detect:
<div class="some-class" (window:resize)="onResize($event)">...</div>
Then in your .ts file you can just add:
onResize(event) {
const innerWidth = event.target.innerWidth;
console.log(innerWidth);
if (innerWidth <= 767) {
...do something
}
}
Add this outside of the ngOnInit() {} unless you wanted the window size on page load.
When you resize your window, you'll see the console.log

Backbone: Attaching event to this.$el and re rendering causes multiple events to be bound

I need to attach an event to the main view element, this.$el. In this case its an 'LI'. Then I need to re render this view sometimes. The problem is if i re render it, it attaches any events in the onRender method that is attached to this.$el each time its rendered. So if i call this.render() 3 times the handler gets attached 3 times. However, if i attach the event to a childNode of this.$el, this does not happen and the events seem to be automatically undelegated and added back on each render. The problem is I NEED to use the main this.$el element in this case.
Is this a bug? Shouldn't this.$el function like the childNodes? Should I not be attaching things to this.$el?
inside the view:
onRender: function(){
this.$el.on('click', function(){
// do something
});
If you're able to use the view's event hash, you could do the following:
var Bookmark = Backbone.View.extend({
events: {
'click': function() {
console.log('bound once')
}
}
...});
If for some reason that's not an option, you could explicitly remove any existing event listeners for this event in the render method, which will prevent the listener from being attached multiple times:
var Bookmark = Backbone.View.extend({
...
render: function(x) {
this.$el.off('click.render-click');
this.$el.html(this.template());
this.$el.on('click.render-click', function () {
console.log('only ever bound once');
});
return this;
}
});

Resources