Using useRef to focus an element, migrating class to hooks question? - javascript

Does anyone knows hows the right approach to pass callback references using react hooks. I'm trying to convert a modal that is built in a class component, to a hook component, but I'm not sure what's the correct way to do it.
onOpen = () => {
this.setState({ isOpen: true }, () => {
// Ref for the button
this.closeButtonNode.focus();
});
this.toggleScrollLock();
};
And this is how I pass the the reference in the code
<ModalContent
buttonRef={(n) => {
this.closeButtonNode = n;
}}
{// More props...}
/>
And the modal content component has the buttonRef like this
<button
type="button"
className="close"
aria-labelledby="close-modal"
onClick={onClose}
ref={buttonRef}
>
<span aria-hidden="true">×</span>
</button>
So when the modal pops I was able to get focus in my button close, with hooks, the only way I managed to replicate the behavior is to add an useEffect hook that listen to the isOpen state like this:
useEffect(() => {
if (isOpen) closeButtonNode.current.focus();
}, [isOpen]);
const onOpen = () => {
setIsOpen(true);
toggleScrollLock();
};
And this is how I pass the prop
const closeButtonNode = useRef(null);
return (
<ModalContent
buttonRef={closeButtonNode}
{// More props...}
/>
)
And I just use it like a regular ref, without passing a callback function, this works but I wonder why it works that way and why I cannot set the focus on the onOpen function like the class based component.
This is the sandbox if you want to check the full code.
https://codesandbox.io/s/hooks-modal-vs-class-modal-bdjf0

Why I cannot set the focus on the onOpen function like the class based component
Because when onOpen function get called open toggle still false and it will get updated after the modal is already opened. Please note that useState doesn't have a second argument (callback) like setState in class based component as you have done to set the focus. That why you needed to use useEffect.
You can test that by setting a delay using setTimeout after you set open to true like so:
const onOpen = () => {
setIsOpen(true);
setTimeout(() => closeButtonNode.current.focus())
};
Although, your approach using useEffect would be maybe better option.
codeSandbox example.

Related

Only trigger UseEffect() in Material UI Dialog when it is in opened

I have a parent component that contains a Material UI Dialog (child). Now this dialog's purpose is to fetch and display data from the REST API.
Currently this works by implementing UseEffect() in the Dialog component. However as soon as the parent component is mounted, the UseEffect() inside child component will kick in. The problem is I will be missing information that are supposed to be passed from the parent to the child itself (because both of this component are mounted at the same time).
Now what got me thinking is, I want that this dialog only be mounted when the button to show it is being clicked. Is that possible?
This is the snippet:
Parent.js
const Parent = React.forwardRef((props, ref) => {
const [openChildDialog, setOpenChildDialog] = useState(false)
useEffect(() => {
// function here. This also going to set value in a state that will be used for the child component
})
const handleOpenChildDialog = () => {
setOpenChildDialog(true)
}
const handleCloseChildDialog = () => {
setOpenChildDialog(false)
}
return (
<Page>
<PageTitle>
Parent Component Test
</PageTitle>
// Dialog will only be mounted when this button is clicked.
<Button onClick={handleOpenChildDialog}> Open the child dialog! </Button>
<ChildDialog
open={openChildDialog}
onClose={handleCloseChildDialog}
/>
</Page>
)
})
If what I am asking is not possible, then I am open to alternative as long as the UseEffect() inside the child dialog component is not immediately executed when the parent is mounted.
to only render the ChildDialog component when it's open, simply wrap it in a conditional:
{ openChildDialog && (
<ChildDialog
open={openChildDialog}
onClose={handleCloseChildDialog}
/>
)}
in terms of your useEffect - you can include an array as the 2nd parameter of a useEffect, and then the funciton will only run when anything in the array changes, for example:
useEffect(() => {
// this will run whenever any state or prop changes, as you haven't supplied a second parameter
})
useEffect(() => {
// this will now only run when openChildDialog changes
// you can easily put a check in here to see if openChildDialog is true to only run on open
}, [openChildDialog])
useEffect(() => {
// an empty array means this will only run when the component is first mounted
}, [])
so to answer your useEffect-inside-child running error, you could do something like:
useEffect(() => {
if (open) {
// do stuff only when the open prop changes to true here
}
}, [open])
UseEffect() behaves so that it is executed when the component is mounted, updated and unmounted. However the solution I see here it is using a conditional to render your child component when your openChildDialog change to true
{ openChildDialog &&
<ChildDialog
open={handleOpenChildDialog}
onClose={handleCloseChildDialog}
/>
}
I leave you this incredible guide so you can see in depth how to use this hook: https://overreacted.io/a-complete-guide-to-useeffect/

Reactjs - How to avoid creating a new clickhandler function in each render

In my react component on a button click, i am passing a parameter to my click handler exactly like this
<a
id={`_primaryAction_${messageObject.id}`}
href="#"
class='message'
onClick={(e: MouseEvent) =>
this.handleClick(e, messageObject)
}
>
I have a usecase where my props are changing and re render is happening . so in each new render this click handler new instance will create. Is there a way to avoid this ?
Edited: removed id and passing wholeObject as it is my use case. Yes this is in loop . This a tag will create for the array of messages.
First of all, do more research to see if the re-rendering is indeed a cause for concern, as it might not be such a big deal performance-wise.
As a solution, you could create another component which you pass the object.
const ActionLink = (props) => {
const {
handleClick,
messageObject,
...otherProps
} = props;
const clickHandler = React.useCallback((e: MouseEvent) => {
handleClick(e, messageObject);
}, [handleClick, messageObject]);
return <a
{...otherProps}
onClick={ clickHandler }
/>;
}
export default ActionLink;
And in your case, you can use it like the following (instead of the a)
<ActionLink
id={`_primaryAction_${messageObject.id}`}
href="#"
class="message"
messageObject={messageObject}
handleClick={this.handleClick} >...</ActionLink>
And if required, you can further protect against re-renders by passing it through React.memo
export default React.memo(ActionLink);
Lastly as an alternative, you could do as others have suggested and provide the id to an attribute of the link, and use that inside the handleClick method to retrieve the correct message from the list
something like
<a
id={`_primaryAction_${messageObject.id}`}
href="#"
class='message'
data-message-id={messageObject.id}
onClick={this.handleClick}
>
and in your handleClick
handleClick(e){
const messageId = e.target.getAttribute('data-message-id');
// assuming your message list is named messageList
// adjust accordingly
const message = messageList.find(({ id }) => id === messageId);
// ... do what you were already doing with the message here
}
checkout useCallback
useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders
https://reactjs.org/docs/hooks-reference.html#usecallback
I think you are using a class component
since you want to pass an object which I think is coming dynamically and not some constant in component (i.e. object is part of a map) and also don’t want to create a new function on every render I would suggest set your button attribute's value as the value of your object and you can access it e.target.value and bind the method than using the inline callback
and it will not create a new function now here's the working example
I see you're using class component. In that case, just move the handler into a separate function.
class MyComponent extends React.Component {
handleClick = (e) => {
this.deleteRow(id, e)
}
render() {
return <button onClick={this.handleClick}>Delete Row</button>
}
}

How do I cause re-rendering to the component

How do I cause re-rendering to the component
function Bookmarks({ data }: any) {
const bookmarkedBlogs = useBookmarks().getBookmarkedBlogs(data.allMdx.nodes);
.....
}
when bookmarks change in the hook
function useBookmarks() {
const [bookmarks, setBookmarks, accessDenied] = useLocalStorage<BlogType['id'][]>('bookmarks', []);
const getBookmarkedBlogs = (blogs: BlogType[]) => {
return blogs.filter(checkIsBookmarked)
};
because as of now, even if I toggle bookmarks, the getBookmarkedBlogs function doesn't execute except in the initial render of the component.
How the implementation of useLocalStorage, and how you toggle bookmarks?
localStorage changes don't notify your every hooks except your make a observer model
if you don't make observer model, toggle bookmark in other hooks or other ways wouldn't notify your this hook, so it don't rerun
Your hook doesn't actually work.
It seems like you want a hook that subscribes to updates from some source outside the React render tree.
Here's a simple example of something that works like that:
function MyComp() {
let eventValue = useEventListener()
return (
<div>
{eventValue}
</div>
)
}
function useEventListener() {
let [ value, setValue ] = React.useState(1)
React.useEffect(() => {
setTimeout(() => setValue(5), 1000)
}, [])
return value
}
What makes it work is that the custom hook invokes a built-in hook when data should change. The React Hooks framework handles the rest.
Instead of a setTimeout, you could subscribe to an event stream, or listen for IPC messages, or something else. But:
The custom hook has to actually return something
The custom hook can only trigger a re-render by invoking a builtin hook
The custom hook must set up its subscribe-to-changes logic on its first invocation, which is often best handled by useEffect configured to run once

window.addEventListener('load',... not firing on Safari?

I need redirect page after it load and get param from URL. I can do by button click.
But I want redirect page automatic (without user input). I am use window.addEventListener('load', () => handleClick()) and it work well on Chrome. But on Safari (desktop and mobile) it not always fire (only sometimes it fire - not always).
I can see this by add alert('Beginning'); in handler - on Chrome this fire automatic after page load, but not on Safari.
How I can solve this?
Thanks!
const handleClick = async (event) => {
alert('Beginning'); //For debugging
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({
param,
});
}
if (typeof window !== `undefined`) {
const param = new URLSearchParams(window.location.search).get('param');
}
const Page = () => {
if (typeof window !== `undefined`) {
window.addEventListener('load', () => handleClick())
}
return (
<section >
<button role="link" onClick={handleClick}> //Only for fallback
Press
</button>
</section>
);
};
export default Page;
The load event probably wouldn't be very reliable, since your component probably wouldn't be mounted until after the DOM is loaded. Instead, you can use the lifecycle methods of React component to respond when your component is mounted to the page. For example:
function Component() {
useEffect(() => alert(1), []);
return (
<h1>hello world!</h1>
);
}
You can find more information about the Effect hook in the documentation. Using it with an empty array makes it work like componentDidMount() for class components.
if you want to fire a function just in loading page and redirect it to another url you can use auto call function like this
(function(){
alert('Beginning');
if (typeof window !== 'undefined') {
const param = new URLSearchParams(window.location.search).get('param');
}
if (param !== 'undefined' && param !== null $$ param!==[]){
window.location.href = 'your_new_url_string?param='+param;
}
})();
I strongly advise that you avoid referencing the DOM directly using listeners such as window.addEventListener or document.addEventListener. In fact, this is why you are using React and not jQuery, to create a virtual DOM instead of charging the real DOM directly and reducing the web performance.
In React, you have React hooks to achieve what you are trying to do. They expose a bunch of lifecycle methods that trigger some events. In your case, useEffect fits your requirements. This hook is similar to componentDidMount() and componentDidUpdate():
By using this Hook, you tell React that your component needs to do
something after render. React will remember the function you passed
(we’ll refer to it as our “effect”), and call it later after
performing the DOM updates. In this effect, we set the document title,
but we could also perform data fetching or call some other imperative
API.
Due to the needs of Strapi payments and Safari mobile (needs to wait for the page load) I've mixed the hook and your approach to ensure that the effect is triggered when the page is loaded.
Applied to your code:
const Page = () => {
useEffect(() => {
window.addEventListener('load', () => handleClick())
}, []);
return (
<section >
<button role="link" onClick={handleClick}> //Only for fallback
Press
</button>
</section>
);
};
export default Page;
Basically, the code will trigger the useEffect function once the DOM tree is loaded and it will execute your handleClick function. The empty array ([]) passed as an argument is called deps; means that you are triggering the effect only once. Of course, it will work in all browsers since it's a React feature.

Using refs with conditional rendering

I have a problem with ref and conditional rendering.
I would like to focus an input tag when I click on a button tag.
Basically, I have this simplified code.
class App extends React.Component {
textInput
constructor(props) {
super(props)
this.state = {isEditing: false}
this.textInput = React.createRef()
}
onClick = () => {
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
}
render () {
let edit = this.state.isEditing ?
(<input type="text" ref={this.textInput} />)
: ""
return (
<div>
<button onClick={this.onClick}>lorem </button>
{edit}
</div>
);
}
}
When I click on the button, the input tag is displayed but the ref textInput is still set to null. Thus I can't focus the input.
I found some workaround like:
set autoFocus property in the input tag
hide the input tag with css when isEditing == false
But actually it is a very basic pattern and I would like to know if there is a clean solution.
Thank you
TL;DR:
Change this:
this.setState({isEditing: !this.state.isEditing})
this.textInput.current.focus();
to this:
this.setState(previousState => ({isEditing: !previousState.isEditing}), () => {
this.textInput.current.focus();
});
Update: Functional Components / Hooks
It's been asked in the comments how to do this with useState and functional components. Rafał Guźniczak's answer explains it, but I wanted to provide a bit more explanation and a runnable example.
You still don't want to read state immediately after setting it, but instead of using a second argument callback to setState, you need to run some code after the state is updated and the component has re-rendered. How do we do that?
The answer is useEffect. The purpose of effects are to synchronize external "things" (for example: imperative DOM things like focus) with React state:
const { useEffect, useRef, useState } = React;
const { render } = ReactDOM;
function App(props) {
const [isEditing, setIsEditing] = useState(false);
const textInputRef = useRef(null);
const toggleEditing = () => setIsEditing(val => !val);
// whenever isEditing gets set to true, focus the textbox
useEffect(() => {
if (isEditing && textInputRef.current) {
textInputRef.current.focus();
}
}, [isEditing, textInputRef]);
return (
<div>
<button onClick={toggleEditing}>lorem </button>
{isEditing && <input type="text" ref={textInputRef} />}
</div>
);
}
render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Details:
You're running into a common problem many people run into with React, which is the assumption that setting state is synchronous. It's not. When you call setState, you're requesting that React update the state. The actual state update happens later. This means that immediately after the setState call, the edit element hasn't been created or rendered yet, so the ref points to null.
From the docs:
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied.
Thank a lot for your answer #rossipedia. I was wondering if I can do it with hooks.
And apparently you can't pass second parameter to useState setter as in setState. But you can use useEffect like this (note second parameter in useEffect):
const [isEditing, setIsEditing] = React.useState(false);
React.useEffect(() => {
if (isEditing) {
textInput.current.focus();
}
}, [isEditing]);
const handleClick = () => setIsEditing(isEditing);
And it worked! ;)
Source: https://www.robinwieruch.de/react-usestate-callback/

Categories

Resources