REACT: Log all methods executed by an event, specifically touchstart and touchmove - javascript

As mentioned in the title, I am using React building an application with flux methodology in mind. How do I log all of the methods launched by a touchmove or touchstart event? Can't seem to find something like this on google...

I think the most value for least amount of code would come from creating your own middleware that looks for events in the action objects as they pass through the dispatch function. Here is some sample code below that works in react-create-app and can be easily modified to suit your purpose:
function createLogger(typeOfEvent) {
return () => (next) => (action) => {
let e;
function isEvent(value) {
return value.hasOwnProperty('bubbles')
}
Object.values(action).map(value => {
if (isEvent(value)) {
e = value;
}
})
if (e.type === typeOfEvent) {
console.log(e);
}
let returnValue = next(action)
return returnValue
}
}
const logger = createLogger('click')
export const store = createStore(reducer, applyMiddleware(logger));
Then make sure you dispatch all your actions with the appropriate event as a value somewhere:
<div className="App" onClick={(e) => store.dispatch({type: 'ACTION', e })}>

Related

Functional component with React.memo() still rerenders

I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.
button.tsx
interface Props {
isActive: boolean;
onClick: () => void;
}
const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
useEffect(() => {
console.log('RERENDER');
}, []);
return (
<S.Button onClick={onClick} isActive={isActive}>
{isActive ? 'Daily stats' : 'All time stats'}
</S.Button>
);
};
export default React.memo(StatsButton);
parent.tsx
const DashboardPage: React.FC = () => {
const {
fetchDailyData,
fetchAllTimeData,
} = useDashboard();
useEffect(() => {
fetchCountry();
fetchAllTimeData();
// eslint-disable-next-line
}, []);
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
return (
<S.Container>
<S.Header>
<StatsButton
onClick={handleClick}
isActive={statsButtonActive}
/>
</S.Header>
</S.Container>
)
}
fetch functions are using useCallback
export const useDashboard = (): Readonly<DashboardOperators> => {
const dispatch: any = useDispatch();
const fetchAllTimeData = useCallback(() => {
return dispatch(fetchAllTimeDataAction());
}, [dispatch]);
const fetchDailyData = useCallback(() => {
return dispatch(fetchDailyDataAction());
}, [dispatch]);
return {
fetchAllTimeData,
fetchDailyData,
} as const;
};
You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.
Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.
Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.
Update:
It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.
function useEventCallback(fn) {
const realFn = useRef(fn);
useEffect(() => {
realFn.current = fn;
}, [fn]);
return useMemo((...args) => {
realFn.current(...args)
}, []);
}
This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});

window.localStorage.setItem not executing in onkeydown callback

I am trying to implement an onkeydown event handler into my react app to close the settings screen by pressing ESC:
useEffect(() => {
document.addEventListener("keydown", handleKeyPress, false);
return () => {
document.removeEventListener("keydown", handleKeyPress, false);
};
}, [])
const saveSettings = () => {
window.localStorage.setItem("storage.ls.version", CURRENT_LOCALSTORAGE_SCHEMA_VERSION);
window.localStorage.setItem("auth.token", authInput);
window.localStorage.setItem("filter.class", classInput);
window.localStorage.setItem(
"filter.subjects",
JSON.stringify(subjectsInput.filter((v: string) => v.trim() !== ""))
);
props.dismiss();
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Escape") {
saveSettings();
}
};
The callback does execute, props.dismiss() (in saveSettings) runs just fine. But the changes don't seem to be saved to localStorage. However, when I use the same saveSettings function on a press of a button (which I have been doing already before), it works and saves as expected.
I wasn't able to find something, but is there a restriction that prevents usage of localStorage in event callbacks? Or is there another reason it doesn't work as expected?
See the comment thread under the question; here's the code that reads the localStorage:
const [authInput, setAuthInput] = useState(window.localStorage.getItem("auth.token") ?? "");
const [classInput, setClassInput] = useState(window.localStorage.getItem("filter.class") ?? "");
const [subjectsInput, setSubjectsInput] = useState(
JSON.parse(window.localStorage.getItem("filter.subjects") ?? "[]")
);
Couple of issues here that are contributing to your issue:
The handleKeyPress function is bound as the event handler for the keydown event in the first useEffect, and so it holds the context at that point in time when it runs. So say you have values in local storage already like authInput = apple, classInput = english and subjectsInput = '', and you change the value to authInput = banana, then you hit ESC, the value that will be stored in authInput will be apple again because that was the initial value of authInput when you attached handleKeyPress to the keydown event in the DOM, and will always be apple! So it 'looks' as if the value isn't changing no matter what you do. To fix this, you need to add handleKeyPress as a dependency of that first useEffect(). This function context changes with every state change, because the saveSettings() function has different values for authInput, classInput and all the other values it's referencing, whenever the state changes. So the first change you need to make is the following:
useEffect(() => {
document.addEventListener("keydown", handleKeyPress, false);
return () => {
document.removeEventListener("keydown", handleKeyPress, false);
};
}, [handleKeyPress]); // add the function as a dependency
handleKeyPress doesn't need to change if the values / functions it's referencing inside doesn't change. So use React's useCallback() to specify this relationship:
const handleKeyPress = useCallback((event) => {
if (event.key === "Escape") {
saveSettings();
}
}, [saveSettings]);
See https://reactjs.org/docs/hooks-reference.html#usecallback for more on this.
Lastly, apply the same treatment to saveSettings() because that function depends on three values:
const saveSettings = useCallback(() => {
window.localStorage.setItem("storage.ls.version", CURRENT_LOCALSTORAGE_SCHEMA_VERSION);
window.localStorage.setItem("auth.token", authInput);
window.localStorage.setItem("filter.class", classInput);
window.localStorage.setItem(
"filter.subjects",
JSON.stringify(subjectsInput.filter((v: string) => v.trim() !== ""))
);
props.dismiss();
}, [authInput, classInput, subjectsInput]);
Whenever the state of authInput, classInput or subjectsInput changes, React will run the cleanup that you specified in the useEffect() and re-run the effect with the new handler that has the correct state values in its run-time context.
If you still don't understand why this is happening, that's ok! Don't fret - I highly encourage you to read how Closures work in Javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures or https://javascript.info/closure
Lastly, I want to mention some code smells for you that you might want to cleanup:
When setting the initial states for all of your state variables, wrap the expensive operation of reading from localStorage in a function so React only runs it once, instead of every re-render.
const [authInput, setAuthInput] = useState(window.localStorage.getItem("auth.token") ?? "");
// instead do this
const [authInput, setAuthInput] = useState(() => window.localStorage.getItem("auth.token") ?? "");
Is subjectsInput guaranteed to be an array when you run filter() on it in saveSettings()? You might want to account for this.
I have a react hook that might help you.
useKeyboardEvents.ts
import { useEffect } from "react";
export type UseKeyboardEventsProps = {
[key in keyof WindowEventMap]?: EventListenerOrEventListenerObject;
};
export const useKeyboardEvents = (value: UseKeyboardEventsProps) => {
useEffect(() => {
Object.entries(value).forEach((item) => {
const [key, funct] = item;
window.addEventListener(
key,
funct as EventListenerOrEventListenerObject,
false
);
});
return () => {
Object.entries(value).forEach((item) => {
const [key, funct] = item;
window.removeEventListener(
key,
funct as EventListenerOrEventListenerObject,
false
);
});
};
}, [value]);
};
Use it like follows. Note: I used keydown like in your code, but I think it would be better to listen to keyup. (replace your useEffect with the following)
useKeyboardEvents({
keydown: (event: any) => {
if (event.key === 'Escape') {
saveSettings();
}
}
});

Is there a way to call useContext in a click callback?

I'm learning React and have a custom Context accessible with a use method:
// In a Provider file.
export const useUsefulObject(): UsefulObject {
return useContext(...)
}
I want to use UsefulObject in a click callback in another file. For convenience, I have that callback wrapped in a method someLogic.
const PageComponent: React.FC = () => {
return <Button onClick={(e) => someLogic(e)} />;
}
function someLogic(e: Event) {
const usefulObject = useUsefulObject();
usefulObject.foo(e, ...);
}
However, VS Code alerts that calling useUsefulObject in someLogic is a violation of the rules of hooks since it's called outside of a component method.
I can pass the object down (the below code works!) but it feels like it defeats the point of Contexts to avoid all the passing down. Is there a better way than this?
const PageComponent: React.FC = () => {
const usefulObject = useUsefulObject();
return <Button onClick={(e) => someLogic(e, usefulObject)} />;
}
function someLogic(e: Event, usefulObject) {
usefulObject.foo(e, ...);
}
The hooks need to be called while the component is rendering, so your last idea is one of the possible ways to do so. Another option is you can create a custom hook which accesses the context and creates the someLogic function:
const PageComponent: React.FC = () => {
const someLogic = useSomeLogic();
return <Button onClick={(e) => someLogic(e)} />
}
function useSomeLogic() {
const usefulObject = useUsefulObject();
const someLogic = useCallback((e: Event) => {
usefulObject.foo(e, ...);
}, [usefulObject]);
return someLogic;
}

Assigning a type to an event handler in a custom hook

I have a custom useEventListener hook that I've derived from here, and I just started learning Typescript, so I decided to rewrite it into a typed version.
Here's the hook that I have,
const useEventListener = (
eventName: string,
handler: any,
) => {
const savedHandler: any = useRef()
useEffect(() => {
savedHandler.current = handler
}, [handler])
useEffect(() => {
const eventListener: any = (event: KeyboardEvent) => savedHandler.current(event)
window.addEventListener(eventName, eventListener);
return () => {
window.removeEventListener(eventName, eventListener)
}
}, [eventName])
}
And I use it as,
const keyPressHandler = useCallback(({ key }: KeyboardEvent) => {
if (key === 'ArrowUp')
// do something
else if (key === 'ArrowDown')
// do something else
})
useEventListener('keydown', keyPressHandler)
It's a relatively simple setup, but I've been banging my head on the typing aspect. Getting rid of the any in
const savedHandler: any = useRef()
or in
const eventListener: any = (. . .
makes the compiler complain, and I don't know how to get around this. What type is savedHandler in this case? And for const eventListener, why does it even need a type?
For the saveHandler you can type the useRef as followed:
const savedHandler = useRef<YOURHTMLELEMENTTYPE>();
For YOURHTMLELEMENTTYPE you can choose any of the following, it depends on the the element your referring to.
For the eventListener I would recommend this post. I think this post describes your exact problem.

Delay React onMouseOver event

I have a list of elements, when hovering one of these, I'd like to change my state.
<ListElement onMouseOver={() => this.setState({data})}>Data</ListElement>
Unfortunately, if I move my mouse over the list, my state changes several times in a quick succession. I'd like to delay the change on state, so that it waits like half a second before being fired. Is there a way to do so?
Here's a way you can delay your event by 500ms using a combination of onMouseEnter, onMouseLeave, and setTimeout.
Keep in mind the state update for your data could be managed by a parent component and passed in as a prop.
import React, { useState } from 'react'
const ListElement = () => {
const [data, setData] = useState(null)
const [delayHandler, setDelayHandler] = useState(null)
const handleMouseEnter = event => {
setDelayHandler(setTimeout(() => {
const yourData = // whatever your data is
setData(yourData)
}, 500))
}
const handleMouseLeave = () => {
clearTimeout(delayHandler)
}
return (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
I have a delayed event handler
</div>
)
}
export default ListElement
I might be a little late for this but I'd like to add to some of the answers above using Lodash debounce. When debouncing a method, the debounce function lets you cancel the call to your method based on some event. See example for functional component:
const [isHovered, setIsHovered] = React.useState(false)
const debouncedHandleMouseEnter = debounce(() => setIsHovered(true), 500)
const handlOnMouseLeave = () => {
setIsHovered(false)
debouncedHandleMouseEnter.cancel()
}
return (
<div
onMouseEnter={debouncedHandleMouseEnter}
onMouseLeave={handlOnMouseLeave}
>
... do something with isHovered state...
</div>
)
This example lets you call your function only once the user is hovering in your element for 500ms, if the mouse leaves the element the call is canceled.
You can use debounce as a dedicated package or get it from lodash, etc:
Useful for implementing behavior that should only happen after a repeated action has completed.
const debounce = require('debounce');
class YourComponent extends Component {
constructor(props) {
super(props);
this.debouncedMouseOver = debounce(handleMouseOver, 200);
}
handleMouseOver = data => this.setState({ data });
render() {
const data = [];
return <ListElement onMouseOver={() => this.debouncedMouseOver(data)}>Data</ListElement>;
}
}
You can create a method that will trigger the onMouseOver event when matching special requirements.
In the further example, it triggers after 500 ms.
/**
* Hold the descriptor to the setTimeout
*/
protected timeoutOnMouseOver = false;
/**
* Method which is going to trigger the onMouseOver only once in Xms
*/
protected mouseOverTreatment(data) {
// If they were already a programmed setTimeout
// stop it, and run a new one
if (this.timeoutOnMouseOver) {
clearTimeout(this.timeoutOnMouseOver);
}
this.timeoutOnMouseOver = setTimeout(() => {
this.setState(data);
this.timeoutOnMouseOver = false;
}, 500);
}
debounce is always the answer if you want to limit the action in a time frame.
Implementation is simple, no need for external libraries.
implementation:
type Fnc = (...args: any[]) => void;
// default 300ms delay
export function debounce<F extends Fnc>(func: F, delay = 300) {
type Args = F extends (...args: infer P) => void ? P : never;
let timeout: any;
return function (this: any, ...args: Args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
usage:
...
/** any action you want to debounce */
function foo(
data: any,
event: React.MouseEvent<HTMLDivElement, MouseEvent>
): void {
this.setState({data});
}
const fooDebounced = debounce(foo, 500);
<ListElement onMouseOver={fooDebounced.bind(null, data)}>
Data
</ListElement>
...
You don't actually have to bind a function, but it's a good habit if you loop through multiple elements to avoid initializing a new function for each element.

Categories