Scroll changes state React - reactjs

Added a change in the color of the appbar when scrolling, but the problem is that the useEffect changes the data array. Every time the color of the appbar changes, the array itself changes.
Can it be rewritten in some other way?
const colorChange = useCallback(() => {
if (window.scrollY >= 200) {
setColor(true)
} else {
setColor(false)
}
}, [])
useEffect(() => {
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [colorChange])
The function I use for random arrays
import _ from 'lodash'
export const shuffle = (array) => {
const random = _.shuffle(array)
return random.slice(0, 2)
}

I found the answer. With ref data no longer changes due to scrolling.
I am attaching the solution:
import React, {useEffect, useRef, useState} from 'react'
const ref = useRef()
const [color, setColor] = useState(false)
ref.current = color
useEffect(() => {
const colorChange = () => {
const show = window.scrollY >= 200
if (ref.current !== show) {
setColor(true)
}
}
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [])

You don't have to pass colorChange in dependencies of the useEffect. no need for the useCallback. you can define a function inside the useEffect only.
useEffect(() => {
const colorChange = () => setColor(window.scrollY >= 200);
window.addEventListener("scroll", colorChange);
return () => window.removeEventListener("scroll", colorChange);
}, []);

Related

How to test window.addEventListener('scroll') in React Testing Library?

I have this hook in React I want to write the unit test but I faced the problem that I don't know how I could cover handleScroll function, how can I go to the useEffect to trigger the scroll event?
I tried fireEvent.scroll but not success.
import { useState, useEffect } from 'react';
const scrollWindow = (
fetchOffset: number,
callback: () => void,
preventFetch?: boolean,
) => {
const [isFetching, setIsFetching] = useState(false);
const handleScroll = () => {
const scrollFromBottom = docElem.scrollHeight - docElem.scrollTop - docElem.clientHeight;
if (scrollFromBottom - fetchOffset > 0) return;
setIsFetching(true);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, preventFetch]);
useEffect(() => {
if (!isFetching) return;
callback();
}, [isFetching]);
return [isFetching, setIsFetching];
};
export default scrollWindow;

How can I render Google Translate Widget only once using useEffect Hook in Next JS Project

import { useEffect } from "react";
const GoogleTranslate = () => {
useEffect(() => {
let addScript = document.createElement("script");
addScript.setAttribute(
"src",
"//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
);
document.body.appendChild(addScript);
window.googleTranslateElementInit = googleTranslateElementInit;
}, []);
const googleTranslateElementInit = () => {
return new window.google.translate.TranslateElement(
{
pageLanguage: "en",
layout: google.translate.TranslateElement.InlineLayout.BUTTON,
},
"google_translate_element"
);
};
return <div id="google_translate_element"></div>;
};
export default GoogleTranslate;
Here is my Component. I have used in a single page. But instead of once I am getting two separate instance of google translate button.
So, I want to know how to render only once this button.
Here is the UI image
Double Google translate button bug
useEffect may run twice despite an empty dependency array in multiple circumstances (for example, if a parent component is re-rendering, or if you have React StrictMode enabled).
Hence, you are probably creating 2 elements inside useEffect.
You could analyze your code and check for potential unwanted re-renders or simply replace useEffect with this useEffectOnce hook:
import { useEffect, useRef, useState } from 'react';
export const useEffectOnce = (effect: () => void | (() => void)) => {
const effectFn = useRef<() => void | (() => void)>(effect);
const destroyFn = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const rendered = useRef(false);
const [, setVal] = useState<number>(0);
if (effectCalled.current) {
rendered.current = true;
}
useEffect(() => {
if (!effectCalled.current) {
destroyFn.current = effectFn.current();
effectCalled.current = true;
}
setVal(val => val + 1);
return () => {
if (!rendered.current) {
return;
}
if (destroyFn.current) {
destroyFn.current();
}
};
}, []);
};
Just create a file with this code, and import and use it instead of useEffect.

Every function called in `useEffect` stack must be wrapped in `useCallback`?

I am new to React and it seems to me that if you use a function inside of useEffect, that entire stack has to be wrapped in useCallback in order to comply with the linter.
For example:
const Foo = ({} => {
const someRef = useRef(0);
useEffect(() => {
startProcessWithRef();
}, [startProcessWithRef]);
const handleProcessWithRef = useCallback((event) => {
someRef.current = event.clientY;
}, []);
const startProcessWithRef = useCallback(() => {
window.addEventListener('mousemove', handleProcessWithRef);
}, [handleProcessWithRef]);
...
});
I'm wondering if there is a different pattern where I don't have to make the entire chain starting in useEffect calling startProcessWithRef be wrapped in useCallback with dependencies. I am not saying it is good or bad, I'm just seeing if there is a preferred alternative because I am new and don't know of one.
The idiomatic way to write your example would be similar to this:
Note the importance of removing the event listener in the effect cleanup function.
TS Playground
import {useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
useEffect(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
return null;
};
If you prefer defining the function outside the effect hook, you'll need useCallback:
import {useCallback, useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
const updateRefOnMouseMove = useCallback(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
useEffect(updateRefOnMouseMove, [updateRefOnMouseMove]);
return null;
};

How to throttle often re-rendered component in react

I want to throttle rendering component which connects to WS and often gets data, which cause its very often re-render.
There is my solution with useMemo hook but I'm not sure that useMemo is designed for such things.
For sure every update of data will cause re-render because is that how useState works, and I have to update this data state.
Do you have maybe some advices or ideas how to throttle re-renders of <DataVisualizator /> Component?
useInterval hook
import { useEffect, useRef } from "react";
export const useInterval = (callback: () => void, delay: number) => {
const savedCallback = useRef<() => void>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
if (savedCallback.current) {
savedCallback.current();
}
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
And component which receive data and should throttle his children
import { useEffect, useMemo, useState } from "react";
import useWebSocket from "react-use-websocket";
import { useInterval } from "../Hooks";
export const WebSockets = () => {
const SOCKET_URL = "wss://someWS";
//data will be kind of Dictionary .eg { "key1": val, "anotherkey: valOther }
const [data, setData] = useState({});
const webSocketOptions = {
shouldReconnect: () => true,
retryOnError: true,
reconnectInterval: 3000,
reconnectAttempts: 5,
onError: (e) => console.log(e),
};
const { sendMessage, lastMessage } = useWebSocket(
SOCKET_URL,
webSocketOptions
);
const handleData = (message: RequestData, data: OrderBookData) => {
// lot of operations to deepClone state and set new with new Data
setData(clonedData);
};
useEffect(() => {
lastMessage && handleData(JSON.parse(lastMessage.data), data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
const [tickThrottle, setTickThrottle] = useState(false);
useInterval(() => {
setTickThrottle(!tickThrottle);
}, 700);
//Throttling with useMemo hook
const throttledDataVisdsualizator = useMemo(
() => <DataVisualizator dataToVisualize={data} />,
// eslint-disable-next-line react-hooks/exhaustive-deps
[tickThrottle]
);
return (
<>
{throttledDataVisdsualizator}
</>
);
};
Solution with useMemo hook
const [tickThrottle, setTickThrottle] = useState(false);
useInterval(() => {
setTickThrottle(!tickThrottle);
}, 700);
//Throttling with useMemo hook
const throttledDataVisdsualizator = useMemo(
() => <DataVisualizator dataToVisualize={data} />,
// eslint-disable-next-line react-hooks/exhaustive-deps
[tickThrottle]
);
return (
<>
{throttledDataVisdsualizator}
</>
);

Custom hook for window resize

I'm working on creating a custom hook that captures the browser window size to let me know if it's mobile or not. At the moment, my issue is React telling me it can't retain the variable value of screenSize within the useEffect hook. How do I get around this?
export default function useIsMobile() {
let screenSize = 0;
useEffect(() => {
window.addEventListener("resize", () => {
screenSize = window.innerWidth;
});
return () => {
window.removeEventListener("resize", () => {
screenSize = window.innerWidth;
})
}
}, [screenSize]);
return screenSize <= 768;
}
You can use the useRef hook to create a variable on the component level, then use the .current property to update it's value.
export default function useIsMobile() {
const screenSize = useRef();
useEffect(() => {
window.addEventListener("resize", () => {
screenSize.current = window.innerWidth;
});
return () => {
window.removeEventListener("resize", () => {
screenSize.current = window.innerWidth;
})
}
}, []);
return screenSize.current <= 768;
}
Note that I also removed the dependency from the useEffect hook because it's not needed. Because it's a ref and not a state, you will use the same variable every time which means that you don't need to re-register the listener.
UPDATE
The way useRef work will not trigger a re-render which is why there is a need to use a useState hook instead.
This code snippet will cause a re-render every time the mode changes.
const getIsMobile = () => window.innerWidth <= 768;
export default function useIsMobile() {
const [isMobile, setIsMobile] = useState(getIsMobile());
useEffect(() => {
const onResize = () => {
setIsMobile(getIsMobile());
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
}
}, []);
return isMobile;
}
Note that I moved getIsMobile outside the component because it doesn't contain any logic related to it and also so it could be called on the default value of the useState hook to save a re-render right when the hook first loads.

Resources