How to throttle often re-rendered component in react - reactjs

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}
</>
);

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;

Unsubscribe from watchPositionAsync with useEffect return function

Using react native with expo-location for a mobile app, I would like to unsubscribe from Location.watchPositionAsync which returns a promise with a remove() method to unsubscribe.
I call the function within a useEffect hooks, but i don't know how to correctly return a cleanup function with the watchPositionAsync promise resolved.
Any suggestions?
import { useState, useEffect } from "react";
import { Text, View } from "react-native";
import * as Location from "expo-location";
export const GpsComponent = function () {
const [location, setLocation] = useState(null);
useEffect(() => {
const positionSubscription = async () => {
const positionSubscribe = await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.BestForNavigation },
(newLocation) => {
setLocation(newLocation);
}
);
return positionSubscribe;
};
/*return () => {
positionSubscription.remove();
console.log("Unsubscribed from WatchPositionAsync");
};*/
}, [setLocation]);
return (
<View>
<Text>{JSON.stringify(location)}</Text>
</View>
);
};
This will create the watchPositionAsync subscription and pass the correct remove function as the cleanup of the useEffect. A dummy subscription is created initially with a nop remove function.
useEffect(() => {
// nop subscription. in case not successful
let subscription = { remove: () => {} }
// subscribe async function
const subscribe = async () => {
return await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.Highest },
(newLocation) => {
setLocation(newLocation)
}
)
}
// return subscription promise
subscribe()
.then(result => subscription = result)
.catch(err => console.warn(err))
// return remove function for cleanup
return subscription.remove
}, [])
I finally found a way to unsubscribe to watchPositionAsync using useRef
import { useState, useEffect, useRef } from "react";
import { Text, View } from "react-native";
import * as Location from "expo-location";
export const GpsComponent = function () {
const [location, setLocation] = useState(null);
const unsubscribe = useRef(() => undefined);
useEffect(() => {
const subscribe= async () => {
const positionSubscription = await Location.watchPositionAsync(
{ accuracy: Location.LocationAccuracy.BestForNavigation },
(newLocation) => {
setLocation(newLocation);
}
);
unsubscribe.current=()=>{positionSubscription?.remove()}
};
return ()=>{unsubscribe.current()}
}, []);
return (
<View>
<Text>{JSON.stringify(location)}</Text>
</View>
);
};
It 's also possible to use an object and modify a property after the async function's promise is resolved.

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 do I bind to an event that is raised in a hook?

Besides prop value updates in a hook, I need to bind to events that get triggered in the hook too. So the consumer of the hook can bind to the event-like addEventListner, removeEventListener. How do I do this?
What I have so far:
import {useState, useEffect} from 'react';
interface MyHookProps {
name: string;
onChange: () => void;
}
const useNameHook = () : MyHookProps => {
const [name, setName] = useState<string>('Anakin');
const onChange = () => {
}
useEffect(() => {
setTimeout(() => {
setName('Vader');
// how to a raise an onChange event here that consumers could bind to?
}, 1000);
}, []);
return {
name,
onChange,
}
}
export default function App() {
const {name, onChange} = useNameHook();
const handleHookChange = () => {
console.info('hook changed', name);
}
return (
<div className="App">
<h1>Hello {name}</h1>
</div>
);
}
I think you can refer to the 'Declarative' pattern here.
Reading this article about 'Making setInterval Declarative with React Hooks' from Dan Abramov really changed my ways of thinking about the React hooks.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
So, my attempts to make this useName hook declarative is like below:
// hooks/useName.ts
import { useEffect, useRef, useState } from "react";
type Callback = (name: string) => void;
const useName: (callback: Callback, active: boolean) => string = (
callback,
active
) => {
// "Listener"
const savedCallbackRef = useRef<Callback>();
// keep the listener fresh
useEffect(() => {
savedCallbackRef.current = callback;
}, [callback]);
// name state
const [internalState, setInternalState] = useState("anakin");
// change the name after 1 sec
useEffect(() => {
const timeoutID = setTimeout(() => {
setInternalState("vader");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
// react to the 'name change event'
useEffect(() => {
if (active) {
savedCallbackRef.current?.(internalState);
}
}, [active, internalState]);
return internalState;
};
export default useName;
and you can use this hook like this:
// App.ts
import useName from "./hooks/useName";
function App() {
const name = useName(state => {
console.log(`in name change event, ${state}`);
}, true);
return <p>{name}</p>;
}
export default App;
Note that the 'callback' runs even with the initial value ('anakin' in this case), and if you want to avoid it you may refer to this thread in SO:
Make React useEffect hook not run on initial render

How does react useEffect work with useState hook?

Can someone explain what am I'm doing wrong?
I have a react functional component, where I use useEffect hook to fetch some data from server and put that data to state value. Right after fetching data, at the same useHook I need to use that state value, but the value is clear for some reason. Take a look at my example, console has an empty string, but on the browser I can see that value.
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Link to codesandbox.
The useEffect hook will run after your component renders, and it will be re-run whenever one of the dependencies passed in the second argument's array changes.
In your effect, you are doing console.log(value) but in the dependency array you didn't pass value as a dependency. Thus, the effect only runs on mount (when value is still "") and never again.
By adding value to the dependency array, the effect will run on mount but also whenever value changes (which in a normal scenario you usually don't want to do, but that depends)
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Not sure exactly what you need to do, but if you need to do something with the returned value from your endpoint you should either do it with the endpoint returned value (instead of the one in the state) or handle the state value outside the hook
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
// handle the returned value here
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
};
fetchData();
}, []);
// Or handle the value stored in the state once is set
if(value) {
// do something
}
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;

Resources