I am building a React component to be wrapped around other components that will grey-out the children and (hopefully) make them unreactive to clicks.
My approach is:
const TierGater: React.FC = (props) => {
const handleTierGaterClick = (e: React.MouseEvent<HTMLDivElement>) => {
console.log("outer - should be called");
// e.stopPropagation();
// e.preventDefault();
};
return (
<div onClick={handleTierGaterClick}>
<div
onClick={() => {
console.log("inner - should not be called");
}}
>
{props.children}
</div>
</div>
);
};
export default TierGater;
I would like this to be a generic solution that allows me to wrap components arbitrarily without needing to modify the onClick of children components.
I have scanned various solutions but my understanding is that event handlers will fire from children to parents order. This is corroborated by console logs for the above example (inner called first).
I believe I can change the children to inspect the event target and abort onClick logic based on that, but this defeats the purpose of the component - it should wrap around any children without modifying the children.
Is the above possible? If yes, what changes would need to be introduced?
Side question: my event handler is typed with React.MouseEvent<HTMLDivElement> - how can I make this work with both mouse and touch events?
With onClickCapture, you can intercept the events in the capturing phase, before they've made it down to the children, and stop them from propagating:
<div onClickCapture={handleTierGaterClick}>
const handleTierGaterClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
};
To be more UI-friendly, a faded background and perhaps pointer-events: none would be good too.
Related
I have a requirement where i have couple of components out of which only one could be displayed at a particular point of time. The components are of varying heights.
When a user clicks on the button in the currently displayed component i need to
Show a loader to the user
Render the component in background.
Wait till the rendered component calls onLoad callback.
Once onLoad callback is recived, hide the loader.
Something like below (Code sandbox here)
const [loading, setLoading] = useState(false);
const [activeElement, setActiveElement] = useState("Component1");
const onLoad = () => {
setLoading(false);
};
const onClick = () => {
setLoading(true);
setActiveElement(
activeElement === "Component1" ? "Component2" : "Component1"
);
};
return (
<div className="container">
<ViewWrapperHOC loading={loading}>
{activeElement === "Component1" ? (
<Component1 onLoad={onLoad} onClick={onClick} />
) : (
<Component2 onLoad={onLoad} onClick={onClick} />
)}
</ViewWrapperHOC>
</div>
);
I was planning to write a wrapper component (ViewWrapperHOC in above example) to show the transition between the components and show the loader. Now the problem is when i try to animate, since i have to render both the progressbar and children in viewwrapper, the animation seems to be very glitchy.
Code sandbox here
Could someone suggest a way to fix this. I am open to any alternate animations or any alternate approach to achieve this in any form of pleasant ux. Thanks in advance.
In that second code sandbox, your issue is that as soon as you click, you are changing which element is active, therefore it gets rendered.
You could put a small timeout in the onClick function:
const onClick = () => {
setLoading(true);
setTimeout(() => {
setActiveElement(
activeElement === "Component1" ? "Component2" : "Component1"
);
}, 100);
};
Then in view wrapper you'll need to get rid of transition: "200ms all ease-in-out, 50ms transform ease-in-out".
You need to have a known height here to be able to make a smooth change in height transition.
See codesandbox fork
Version 2:
Here is version 2, using refs. Main changes are moving the box shadow out of the components to the container in view wrapper. Adding refs to the components, passing the active ref to the wrapper and then setting the height with height transition.
To be honest, I'd probably restructure the code a lot if I had the time.
Typically, when creating a reusable React component that we want to conditionally render, we'll either give it a prop to tell it whether or not to render itself:
function TheComponent(props) {
return(
props.isVisible?<div>...</div>:null
);
}
Or just render the whole component conditionally, from the outside:
function App() {
//...
return (
isVisible ? <TheComponent /> : null
);
}
Alternatively, if we want to make a component that we can show/hide from anywhere in our application - like a toast notification - we could wrap in a provider & make a custom hook to access its context; this would let us show/hide it from anywhere inside the provider, just by calling a function:
const App = () => (
<ToastProvider>
<OtherStuff />
</ToastProvider>
);
const OtherStuff = () => {
const { showToast } = useToast();
showToast();
return ...;
};
However, there's a really cool package, react-toastify, that I can't seem to wrap my head around how it's implemented. All you have to do is drop a <ToastContainer /> somewhere in your app, then from anywhere else, you can:
import { toast } from "react-toastify";
toast.info("this will show the component with a message");
Since this function can be called outside of a provider, I don't really understand how it's controlling the state of the component elsewhere in the tree. I've tried looking into its code, but as a React beginner, it's a bit over my head. I love the idea of a totally self-contained component that you can just stick somewhere in your app, and invoke by calling a function from anywhere. No Provider/wrapper or anything: just a function call, and out it pops.
Can someone help shed some light on how, fundamentally, a component like this could work? How can a function outside of a provider be controlling state inside another component?
Glancing at the react-toastify code you can see it’s using an event emitter pattern. The <ToastContainer /> listens for events that get dispatched (or emitted) when you call toast.info. When it gets an event it updates its internal state (presumably) to show the message.
TLDR: They're communicating indirectly through the eventManager, which exposes methods for 1) dispatching events and 2) registering listeners for those events.
It's similar to the way an onclick handler works in the DOM.
Here's a very rudimentary implementation of the basic pattern: It just appends a div to the document each time the button is clicked. (This isn't React- or toastify-specific. But it demonstrates the core idea.)
Notice that the button's click handler doesn't know anything about what happens. It doesn't append the div. It just emits an event via the EventBus instance described below.
The EventBus class provides an on method to register a listener. These are often called addEventListener or addListener, or they have an event-specific name like onClick, onChange, etc., but they all do the same basic thing: register a function to be invoked in response to an event. (This class is essentially a dumber implementation of react-toastify's eventManager.)
The on method adds the provided handler to an internal array. Then, when an event is fired (via emit) it just iterates over the array invoking each one and passing in the event information.
const container = document.getElementById('demo');
const button = document.querySelector('button');
class EventBus {
handlers = [];
on (handler) {
this.handlers.push(handler);
}
emit (event) {
this.handlers.forEach(h => h(event));
}
}
const emitter = new EventBus();
emitter.on((event) => {
container.innerHTML += `<div>${event}</div>`;
})
button.addEventListener('click', () => emitter.emit('Button Clicked'));
<button>Emit</button>
<div id="demo"></div>
With this setup you can add additional listeners to do other things without having to know where the event originates (the button click). The demo below is the same as above except it adds an additional handler to toggle "dark" mode.
Again, notice that the button doesn't know about dark mode, and the dark mode handler doesn't know about the button, and neither of them know about the div being appended. They're completely decoupled.
This is basically how the ToastContainer works with toast.info.
const container = document.getElementById('demo');
const button = document.querySelector('button');
class EventBus {
handlers = [];
on (handler) {
this.handlers.push(handler);
}
emit (event) {
this.handlers.forEach(h => h(event));
}
}
const emitter = new EventBus();
emitter.on((event) => {
container.innerHTML += `<div>${event}</div>`;
})
button.addEventListener('click', () => emitter.emit('Button Clicked'));
// add an additional handler
emitter.on(event => demo.classList.toggle('dark'));
.dark {
background: black;
color: white;
}
<button>Emit</button>
<div id="demo"></div>
I have a component where I have to pass a reference of a button to it's children like this:
function Parent() {
const ref = useRef();
return (
<>
<Child button={ref} />
<button ref={ref}>Do something</button>
</>
);
}
then in the child component I am adding an event listener like this
function Child({ button }) {
useEffect(() => {
button.current.addEventListener("click", someFunction);
return ()=>button.current.removeEventListener("click", someFunction);
}, []);
....
}
someFunction is a function that I am using from a third party library(react-hook-form) which calls e.persist()
function someFunction(e){
e.persist();
....
}
because I am passing it from the addEventListener a normal event is being passed instead of a synthetic one so I am getting an error that e.persist is not a function, how can I solve this or what is a better approach to this?
Note that the button has to be where it is, I can't create it inside that Child component itself because on different screens it has different functions, and <Child/> is just one of the screens, I am switching between them based on a condition
While I know, that this question has been asked numerous time, the usual answer is "you're passing a copy, not the original". Unless I'm understanding something wrong, this is not the problem in my case.
I made a Modal in React using the following code:
const Modal: React.StatelessComponent<Props> =
({ isOpen, closeModal, closeOnClick = true, style, children }: Props) => {
const closeOnEsc: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
e.stopImmediatePropagation();
if (e.keyCode === 27) {
closeModal();
}
};
if (isOpen) {
document.body.classList.add('freezeScroll');
window.addEventListener('keyup', closeOnEsc, { once: true });
return (
<div className={styles.modal} onClick={closeOnClick ? closeModal : undefined} >
<div className={`${styles.modalContent} ${closeOnClick ? styles.clickable : undefined} ${style}`} >
{children}
</div>
</div>
);
}
document.body.classList.remove('freezeScroll');
window.removeEventListener('keyup', closeOnEsc, { once: true });
return null;
};
I've only added the {once: true} option, because I can't remove it otherwise.
window.removeEventListener does get called once the modal is closed, but it doesn't remove the event listener.
Can anyone help me figure out why?
I wouldn't manually bind listeners in a SFC personally. If you really want to though, I would just make the callback remove the listener.
In your closeOnEsc callback, add window.removeEventListener('keyup', closeOnEsc);
The problem with this, if the component is unmounted without the callback being invoked, then you have a small memory leak.
I would use a class component, and ensure the listener is also removed in the componentWillUnmount lifecycle hook.
You should not attach event listener directly inside render. These are not idempotent side effects and it will cause memory leak. On each render (even if it is not commited to DOM), you are assigning event listeners After your modal is closed, you remove only last one. Right way to do it would be either using useEffect (or useLayoutEffect hook), or converting your component into class and using lifecycle methods.
Here is annotated example with hooks:
const Modal: React.StatelessComponent<Props> = ({
isOpen,
closeModal,
closeOnClick = true,
style,
children,
}: Props) => {
useEffect(
function () {
// we don't need to add listener when modal is closed
if (!isOpen) return;
// we are closuring function to properly remove event listener later
const closeOnEsc: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
e.stopImmediatePropagation();
if (e.keyCode === 27) {
closeModal();
}
};
window.addEventListener("keyup", closeOnEsc);
// We return cleanup function that will be executed either before effect will be fired again,
// or when componen will be unmounted
return () => {
// since we closured function, we will definitely remove listener
window.removeEventListener("keyup", closeOnEsc);
};
},
[isOpen]
);
useEffect(
function () {
// it is very tricky with setting class on body, since we theoretically can have multiple open modals simultaneously
// if one of this model will be closed, it will remove class from body.
// This is not particularly good solution - it is better to have some centralized place to set this class.
// This is why I moved it into separate effect
if (isOpen) {
document.body.classList.add("freezeScroll");
} else {
document.body.classList.remove("freezeScroll");
}
// just in case if modal would be unmounted, we reset body
return () => {
document.body.classList.remove("freezeScroll");
};
},
[isOpen]
);
if (isOpen) {
return (
<div
className={styles.modal}
onClick={closeOnClick ? closeModal : undefined}
>
<div
className={`${styles.modalContent} ${
closeOnClick ? styles.clickable : undefined
} ${style}`}
>
{children}
</div>
</div>
);
}
return null;
};
Again. It is very important that render method of classes and function components don't have any side effects that are not idempotent. Idempotence in this case means that if we call some side effect multiple times, it will not change the outcome. This is why you can call console.log from render - it is idempotent, at least from our perspective. Same with react hooks. You can (and should!) call them on each render without messing up react internal state. Attaching new event listener to DOM or window is not idempotent, since on each render you attach new event listener without clearing previous one.
Technically, you can first remove event listener, and then attach new without useEffect, but in this case you will have to preserve listener identity (reference) between renders somehow, or remove all keyup events from window at once. Former is hard to do (you are creating new function on every render), and latter can interfere with other parts of your program.
I occasionally have react components that are conceptually stateful which I want to reset. The ideal behavior would be equivalent to removing the old component and readding a new, pristine component.
React provides a method setState which allows setting the components own explicit state, but that excludes implicit state such as browser focus and form state, and it also excludes the state of its children. Catching all that indirect state can be a tricky task, and I'd prefer to solve it rigorously and completely rather that playing whack-a-mole with every new bit of surprising state.
Is there an API or pattern to do this?
Edit: I made a trivial example demonstrating the this.replaceState(this.getInitialState()) approach and contrasting it with the this.setState(this.getInitialState()) approach: jsfiddle - replaceState is more robust.
To ensure that the implicit browser state you mention and state of children is reset, you can add a key attribute to the root-level component returned by render; when it changes, that component will be thrown away and created from scratch.
render: function() {
// ...
return <div key={uniqueId}>
{children}
</div>;
}
There's no shortcut to reset the individual component's local state.
Adding a key attribute to the element that you need to reinitialize, will reload it every time the props or state associate to the element change.
key={new Date().getTime()}
Here is an example:
render() {
const items = (this.props.resources) || [];
const totalNumberOfItems = (this.props.resources.noOfItems) || 0;
return (
<div className="items-container">
<PaginationContainer
key={new Date().getTime()}
totalNumberOfItems={totalNumberOfItems}
items={items}
onPageChange={this.onPageChange}
/>
</div>
);
}
You should actually avoid replaceState and use setState instead.
The docs say that replaceState "may be removed entirely in a future version of React." I think it will most definitely be removed because replaceState doesn't really jive with the philosophy of React. It facilitates making a React component begin to feel kinda swiss knife-y.
This grates against the natural growth of a React component of becoming smaller, and more purpose-made.
In React, if you have to err on generalization or specialization: aim for specialization. As a corollary, the state tree for your component should have a certain parsimony (it's fine to tastefully break this rule if you're scaffolding out a brand-spanking new product though).
Anyway this is how you do it. Similar to Ben's (accepted) answer above, but like this:
this.setState(this.getInitialState());
Also (like Ben also said) in order to reset the "browser state" you need to remove that DOM node. Harness the power of the vdom and use a new key prop for that component. The new render will replace that component wholesale.
Reference: https://facebook.github.io/react/docs/component-api.html#replacestate
The approach where you add a key property to the element and control its value from the parent works correctly. Here is an example of how you use a component to reset itself.
The key is controlled in the parent element, but the function that updates the key is passed as a prop to the main element. That way, the button that resets a form can reside in the form component itself.
const InnerForm = (props) => {
const { resetForm } = props;
const [value, setValue] = useState('initialValue');
return (
<>
Value: {value}
<button onClick={() => { setValue('newValue'); }}>
Change Value
</button>
<button onClick={resetForm}>
Reset Form
</button>
</>
);
};
export const App = (props) => {
const [resetHeuristicKey, setResetHeuristicKey] = useState(false);
const resetForm = () => setResetHeuristicKey(!resetHeuristicKey);
return (
<>
<h1>Form</h1>
<InnerForm key={resetHeuristicKey} resetForm={resetForm} />
</>
);
};
Example code (reset the MyFormComponent and it's state after submitted successfully):
function render() {
const [formkey, setFormkey] = useState( Date.now() )
return <>
<MyFormComponent key={formkey} handleSubmitted={()=>{
setFormkey( Date.now() )
}}/>
</>
}
Maybe you can use the method reset() of the form:
import { useRef } from 'react';
interface Props {
data: string;
}
function Demo(props: Props) {
const formRef = useRef<HTMLFormElement | null>(null);
function resetHandler() {
formRef.current?.reset();
}
return(
<form ref={formRef}>
<input defaultValue={props.data}/>
<button onClick={resetHandler}>reset</button>
</form>
);
}