I would like to create a custom synthetic event so that I can conditionally attach a handler (either a touchstart or click), based on a configuration option
However, I cannot seem to find any information on how this can be done
Ideally I would like the below (the onTap attribute)
<Button onTap={someHandler} title="Register" />
And then the onTap would attach the handler either on touchstart or click or any event I define in a config
Is this possible?
Is it possible to define a custom attribute that will hook on every component?
Regards
You could try something like this:
const W = (() => {
// all elements supported by React
const names = 'a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|main|map|mark|menu|menuitem|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|source|span|strong|style|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr|circle|clipPath|defs|ellipse|g|image|line|linearGradient|mask|path|pattern|polygon|polyline|radialGradient|rect|stop|svg|text|tspan'.split('|')
const res = {}
for (const El of names) {
res[El] = ({ onTap, ...props }) => {
onTap = onTap || x=>x
props.onClick = props.onClick || x => x
props.onTouchStart = props.onTouchStart || x => x
<El {...props} onClick={(...args) => {onTap(...args); props.onClick(...args)} onTouchStart={(...args) => {onTap(...args); props.onTouchStart(...args)} />
}
}
return res;
})()
<W.button onTap={() => alert('hi')} />
This will add the onTap handler to both the onClick and onTouchStart events of any element.
A similar technique can be used to wrap composite components.
To wrap every component, you need to wrap React.createElement.
Warning: I make no guarantees about if this will work. It is probably a very bad idea, and should not be used in a library.
const _ce = React.createElement.bind(React)
React.createElement = (name, props, ...args) => {
if (!props) {
return _ce(name, props, ...args)
}
const { onTap, ...newProps } = props
if (onTap) {
if (props.onClick) {
newProps.onClick = (...args) => {
props.onClick(...args)
onTap(...args)
}
} else {
newProps.onClick = onTap
}
if (props.onTouchStart) {
newProps.onTouchStart = (...args) => {
props.onTouchStart(...args)
onTap(...args)
}
} else {
newProps.onTouchStart = onTap
}
}
return _ce(name, newProps, ...args)
}
Related
I want to create a generic react hook that will add a scroll event to the element and return a boolean indicating that the user has scrolled to the top of the element.
Now, the problem is this element might not be visible right away. Hence I'm not able to use useEffect. As I understand in that situation it is advised to use useCallback
So I did, and it works:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const setRef = useCallback((element) => {
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (element) {
element.addEventListener("scroll", handleScroll);
}
ref.current = element;
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
I can use my hook like this:
const { hasScrolled, scrollingElementRef } = useHasScrolled();
....
return <div ref={scrollingElementRef}>....
However, the problem is, I don't know how to remove the event listener. With the useEffect hook, it's pretty straightforward - you just return the cleanup function.
Here's the codesandbox, if you want to check the implementation: https://codesandbox.io/s/pedantic-dhawan-83fdw3
Expected behavior - when node is removed from DOM - event listeners will be also removed and collected by GC.
But
Codesandbox example is a bit tricky, React treats
<div>Loading...</div>
and
<div className="scrollingDiv" ref={scrollingElementRef}>
<h1>Hello, I've finally loaded!</h1>
<Lorem />
</div>
as a same div, same object, just with different props (className and children), so when div.scrollingDiv is replaced by conditional rendering to div(loading) - event listeners are still there and accumulating.
This behavior can be fixed as is by using keys.
{loading ? (
<div key="div1">Loading...</div>
) : (
<div key="div2" className="scrollingDiv" ref={scrollingElementRef}>
<h1>Hello, I've finally loaded!</h1>
<Lorem />
</div>
)}
In that way event listeners will be removed as expected.
Another solution is to add 1 more useRef and useEffect to the custom hook to store and execute actual unsubscribe function:
function useHasScrolled() {
const [hasScrolled, setHasScrolled] = useState(false);
const ref = useRef(null);
const unsubscribeRef = useRef(null);
const setRef = useCallback((element) => {
const eventName = "scroll";
const handleScroll = (e) => {
setHasScrolled(e.target.scrollTop !== 0);
};
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
if (element) {
element.addEventListener(eventName, handleScroll);
unsubscribeRef.current = () => {
console.log("removeEventListener called on: ", element);
element.removeEventListener(eventName, handleScroll);
};
ref.current = element;
} else {
unsubscribeRef.current = null;
ref.current = null;
}
}, []);
useEffect(() => {
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, []);
return {
hasScrolled,
scrollingElementRef: setRef
};
}
That code will work without adding key.
Utility code for Chrome dev console to count scroll listeners:
Array.from(document.querySelectorAll('*'))
.reduce(function(pre, dom){
var clks = getEventListeners(dom).scroll;
pre += clks ? clks.length || 0 : 0;
return pre
}, 0)
Updated codesandbox: https://codesandbox.io/s/angry-einstein-6fb1u4?file=/src/App.js
I created a button in solid-js and I would like that when this button is clicked, a page is opened and the counter goes to zero. To perform these operations, I have already created two functions. I would like to ensure that when clicking this button that these two functions are called asynchronously. So I wrote the following code:
<button
onClick={[toggle, setNullCount]}
>
with functions to call toggle and setNullCount. However, I realized that when I click on the button, only the first declared function is called and I don't know how to allow two functions to be called on click.
Here is a solution
onClick={() => {toggle(); setNullCount()}}
when you want assign more than one action to trigger you must create a function which handles methods that will be performed as effect.
You need to create a single handler function that calls both handlers:
<button
onClick={() => {
toggle();
setNullCount();
}}
>
A wrapper function is not equivalent to adding multiple listeners and it is not always feasible, i.e when you want to handle each events differently.
An alternative could be adding listeners to the element using its reference:
import { onCleanup, onMount } from 'solid-js';
import { render } from 'solid-js/web'
const App = () => {
let el: HTMLButtonElement | undefined;
const handleOne = (event: any) => {
console.log(`Event One`);
};
const handleTwo = (event: any) => {
console.log(`Event Two`);
};
onMount(() => {
el?.addEventListener('click', handleOne);
el?.addEventListener('click', handleTwo);
});
onCleanup(() => {
el?.removeEventListener('click', handleOne);
el?.removeEventListener('click', handleTwo);
});
return (
<div>
<button ref={el}>Click</button>
</div>
)
};
render(App, document.body);
You can see the live demo: https://playground.solidjs.com/anonymous/86afc8e3-3574-40ed-88f7-2e5b467f6b9a
Element is guaranteed to exist inside onMount and onCleanup effects but I didn't want to suppress the type system by a non-null assertion so used optional chaining operator, ?.
Another alternative could be event delegation:
import { onCleanup, onMount } from 'solid-js';
import { render } from 'solid-js/web'
const App = () => {
const handleOne = (event: any) => {
console.log(`Event One`);
};
const handleTwo = (event: any) => {
console.log(`Event Two`);
};
onMount(() => {
document.body.addEventListener('click', handleOne);
document.body.addEventListener('click', handleTwo);
});
onCleanup(() => {
document.body.removeEventListener('click', handleOne);
document.body.removeEventListener('click', handleTwo);
});
return (
<div>
<button>Click</button>
</div>
)
};
render(App, document.body);
Click the link to see the live demo: https://playground.solidjs.com/anonymous/434364d4-c467-427f-8709-3e10557e0b9e
A third alternative could be using so called on:weirdEvent syntax:
import { render } from 'solid-js/web'
const App = () => {
const handleOne = (event: any) => {
console.log(`Event One`);
};
const handleTwo = (event: any) => {
console.log(`Event Two`);
};
return (
<div>
<button on:click={handleOne} on:click={handleTwo}>Click</button>
</div>
)
};
render(App, document.body);
For any other events, perhaps ones with unusual names, or ones you wish not to be delegated, there are the on namespace events. This attribute adds an event listener verbatim.
https://www.solidjs.com/docs/latest/api#on___oncapture___
You can find the live demo: https://playground.solidjs.com/anonymous/30c32d5b-3185-45ab-987d-15f2bf9c8f98
A fourth alternative could be using a custom directive. A custom directive receives the element when the element gets created in the DOM. So, you can attach the listener on the element itself or use event delegation. I will go with the former one here:
import { Accessor, onCleanup } from 'solid-js';
import { render } from 'solid-js/web';
interface Handlers {
handlerOne: (event: MouseEvent) => void;
handlerTwo: (event: MouseEvent) => void;
}
export const multipleHandlers = (el: HTMLButtonElement, args: Accessor<Handlers>) => {
el.addEventListener("click", args().handlerOne);
el.addEventListener("click", args().handlerTwo);
onCleanup(() => el.removeEventListener("click", args().handlerOne));
onCleanup(() => el.removeEventListener("click", args().handlerTwo));
}
declare module "solid-js" {
namespace JSX {
interface Directives {
multipleHandlers: Handlers;
}
}
}
const App = () => {
const handlerOne = (event: any) => {
console.log(`Event One`);
};
const handlerTwo = (event: any) => {
console.log(`Event Two`);
};
return (
<div>
<button use:multipleHandlers={{ handlerOne, handlerTwo }}>Click</button>
</div>
)
}
render(App, document.body);
https://playground.solidjs.com/anonymous/d6cbfe71-d657-4749-9126-a5fc5984a334
Explicit being better than implicit, I advice you to use any of the above methods over a custom directive. Custom directives could be a good alternative only when you find yourself in need to add multiple listeners to the same element frequently.
I have two components, such that they are:
const ChildOn = (/*...*/) => {
//...
}
const Parent = () => {
const [is42, setIs42] = useState(true)
return (is42 ? <ChildOff ... > : <ChildOn ... />)
}
The definition of ChildOff is no important.
I want to define them as either of the following, yet I can't decide which:
Declares functions used in children, based on a variable/function in the parent, inside each child.
type ChildOnProp = { setIs42: Dispatch<SetStateAction<boolean>> };
const ChildOn = ({ setIs42 }: ChildOnProp) => {
const f1 = () => { setIs42(true); };
return <Text onPress={f1} />;
};
const Parent = () => {
return is42
? <ChildOff setIs42={setIs42} />
: <ChildOn setIs42={setIs42} />;
};
Defines functions used by children, inside the parent.
type ChildOnProps = { func: () => void }
const ChildOn = ({ func }: ChildOnProps) => {
return <Text onPress={func} />
}
const Parent = () => {
const [is42, setIs42] = useState(true)
const f1 = useCallback(() => { setIs42(true) })
const f2 = useCallback(() => { setIs42(false) })
return (is42 ? <ChildOff func={f2} /> : <ChildOn func={f1} />)
}
While (1) is much prettier to me, (2) seems a lot more efficient. Yet I don't know if I'm apt to judge that, since I've read many articles on React contradicting each other on when it's best to use useCallback.
The React community warns against useless memoizing as it could add complexity without any of the benefits in some situations.
In this case, I think it's worth memoizing the callbacks since it will reduce the number of unnecessary renders of the child components, which otherwise could have negative impact, or even introduce issues down over time.
To do this, there's a common pattern of creating a custom hook, often called useToggle, which memoize common setters.
import {useState, useMemo} from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// Defined once, so guaranteed stability
const setters = useMemo(() => ({
toggle: () => setValue(v => !v),
setFalse: () => setValue(false),
setTrue: () => setValue(true),
setValue,
}), [setValue]);
// Defined each time the value changes, so less than every render.
return useMemo(() => ({
...setters,
value
}), [value, setters]);
}
This can be used in the parent as such:
const Parent = () => {
const { value: is42, setTrue, setFalse } = useToggle(true);
return (is42 ? <ChildOff func={setFalse}> : <ChildOn func={setTrue} />);
}
TypeScript will infer all the types automatically, so it works out of the box.
If there's multiple state values that need a toggle callback, we can easily identify each one by not destructuring.
const firstState = useToggle(true);
const secondState = useToggle(true);
//...
return (
<>
<Foo onClick={firstState.toggle} />
<Bar onClick={secondState.toggle} />
</>
);
As a reference, here's a simpler implementation from Shopify.
As for memoizing too much or too little, Meta (Facebook) is apparently experimenting with memoizing everything by default at transpile time, so that devs wouldn't have to think about it.
I'm having problems with setting up lodash debounce in the function to make an API request. For some reason callback doesn't happen and the value sends every time I type.
import debounce from "lodash/debounce";
const handleChange = (event) => {
const { value } = event.target;
const debouncedSave = debounce((nextValue) => dispatch(movieActions.getMovies(nextValue), 1000));
debouncedSave(value);
};
I'm using material ui and have this in return:
<Autocomplete
onInputChange={handleChange}
/>
Your debounced function is created multiple times for each change event and that causes the problem. I will use a simplified example with a simple input and a console.log instead of your dispatch, but you can apply the solution to your case as well.
The simplest solution would be to move the debouncedSave declaration outside your component.
const debouncedSave = debounce((nextValue) => console.log(nextValue), 1000);
export default function App() {
const handleChange = (e) => {
const { value } = e.target;
debouncedSave(value);
};
return <input onChange={handleChange} />;
}
or else if you want to keep the debounced function declaration inside your component you can use a ref, to create and use the same instance each time, no matter the re-renders:
export default function App() {
const debouncedSaveRef = useRef(
debounce((nextValue) => console.log(nextValue), 1000)
);
const handleChange = (e) => {
const { value } = e.target;
debouncedSaveRef.current(value);
};
return <input onChange={handleChange} />;
}
I've got two splitted functions and I need to pass a value from one to the other, which I'm doing like shown below. What is the difference between const handleClick = icon.onClick(category) and const handleClick = () => icon.onClick(category)?
And how do I pass the event from the component to the handleClick() function?
export const useCategories = () => {
const handleClick = (category, something) => {
event.stopPropagation() // <-- 3. How to get event?
console.log(category, something) // <-- 4. Get every value
}
return {
icon: {
onClick: (category) => handleClick(category, 'anything') // <-- 2. add second var value
}
}
}
export const Categories = () => {
const { icon } = useCategories()
return (
<div>
{categories.map((category) => {
const handleClick = icon.onClick(category) // <-- 1. pass category value
return <Icon onClick={handleClick} />)}
}
</div>
)
}
You'll need to proxy the event object through on all click handlers. I like to use curried functions to make attaching the click handler a little simpler. Don't forget to add a react key to the mapped icons.
export const useCategories = () => {
const handleClick = (event, category, something) => {
event.stopPropagation();
console.log(category, something);
};
return {
icon: {
// curried function to receive category and return onClick handler
onClick: category => event => handleClick(event, category, 'anything'),
}
}
}
export const Categories = () => {
const { icon } = useCategories();
return (
<div>
{categories.map((category, index) => (
<Icon
key={index}
onClick={icon.onClick(category)} // <-- set category
/>
)
</div>
);
}
The onevent handlers are properties on certain DOM elements to manage how that element reacts to events.
When the event handler is specified as an HTML attribute, the specified code is wrapped into a function with the following parameters:
event — for all event handlers except onerror.
event, source, lineno,colno, and error for the onerror event handler.
Note that the event parameter actually contains the error
message as a string.
When the event handler is invoked, the this keyword inside the handler is set to the DOM element on which the handler is registered. For more details, see, see the this keyword documentation.
if you want more see, this
but in your code, you need to add this
<Icon onClick={(event) => icon.onClick(event, category, ...rest)} />
As you want to pass the event and category to the click handler, modify the function passed to onClick to pass the args.
export const useCategories = () => {
const handleClick = (event, ...rest) => {
event.stopPropagation();
console.log(rest);
};
return {
icon: {
onClick: (event, category) => handleClick(event, category, 'anything'),
},
};
};
export const Categories = () => {
const { icon } = useCategories();
return (
<div>
{categories.map((category) => {
return <Icon onClick={(event) => icon.onClick(event, category)} />;
})}
</div>
);
};
When you dont need to pass parametrs you use
onClick={func}
When you want to pass parametrs you use
onClick={() => func(someParmeter)}
To pass the event simply write
onClick={e => func(e)}