useLayoutEffect With Practical Example
- Published on
- Updated on
If you want to know when you need to use useLayoutEffect
rather than useEffect
this post is for you. I am going to show it with a practical example, that is placing the caret in a contenteditable div element with the help of useLayoutEffect hook.
Introduction
You may know the fact that we cannot highlight words within <textarea>
element. I mean we are not able to style text within textarea. And if we need such a feature in a React application and if we don't use a 3rd party text editor library like draft.js, then we can use <div contenteditable="true">
that is a div element and it can be editable by user (it does not have to be div element it can be pre
element or so).
One example why you need contenteditable instead of textarea, it is supposed you are building a comment component for your blog site, that is your readers can add comments with this component. And suppose you have some kind of dictionary integration with this comment component, such that while your user types something he or she can see some of unrestricted words or say he/she can see the words written incorrect (without AI) depending on response of the dictionary that you are using.
However in order to use a contenteditable element like a textarea element (thus we can retrieve text data and send it to backend or vice versa) we need to do some extra effort. Because unfortunately a contenteditable element cannot behave exactly as a textarea element.
Problem
If you have a contenteditable div
in React application with dangerouslySetInnerHTML
then you are going to see that your cursor is not at the end of the text but it is at the beginning of the text, and while you type some words, they are written in reverse. In order to fix this we need to place the cursor on every change at the back of the text. And suppose you add some placeCaret logic in your component's code. But this time you are going to face the jumping cursor problem.And to fix that we need to use the useLayoutEffect
hook.
In fact in this blog first I am going to show how you can create a custom textarea element, that is a simple text editor with an editable div element, and then I am going to show you the cursor placement problem I encountered it, and then I am going to show how we can fix it.
In this part we are going to create simple text editor with div contenteditable with inputchange event.
import React, { useRef } from 'react'
const TextArea = ({ highlighted, ...rest }) =>
const editorRef = useRef();
//onInputChnage callback function
const handleInputChange = (_) => {
const editor = editorRef.current
console.log(editor.innerText)
}
return (
<div
className="textarea"
contentEditable
placeholder="Enter your text here"
ref={editorRef}
onInput={handleInputChange}
spellCheck={false}
{...rest}
/>
)
}
export default TextArea
For detecting changes inside of an editable element we need to listen oninput
change event instead of onchange
event.
dangerouslySetInnerHTML
To keep the component's state in sync, we need to use dangerouslySetInnerHTML
, or you can update value by setting innerHTML
of editorRef.current
but it is not a good practice.
In the following code block, I added also the highlighting logic but you don't have to focus on that part, you can find the details of the utils
functiopn from the source cod that its link in the final code sction:
import React, { useRef, useState } from 'react'
import { getTextSegments, highlightText } from '../utils'
const TextArea = ({ highlighted, ...rest }) => {
const editorRef = useRef()
const [lastHtml, setLastHtml] = useState()
//onInputChnage callback function
const handleInputChange = (_) => {
const editor = editorRef.current
const textSegments = getTextSegments(editor)
const textContent = textSegments.map(({ text }) => text).join('')
let html = highlightText(textContent, highlighted) // returns the text with highlighted words
setLastHtml(html) // sets the html of dangerouslySetInnerHTML
}
return (
<div
className="textarea"
contentEditable
placeholder="Enter your text here"
ref={editorRef}
onInput={handleInputChange}
spellCheck={false}
dangerouslySetInnerHTML={{ __html: lastHtml }}
{...rest}
/>
)
}
export default TextArea
If we run our application with the code above we can see that our caret is not behave normal it places at the beginnig of the text and it seems you are writing the words in reverse. See the below gif:
Jumping Caret
In this part I am going to add a place caret helper function, that replaces the caret at the end of the text on every time when contenteditable's html changes... Sounds like we need to use
useEffect
, right? Let's see:
import React, { useRef, useState, useEffect } from 'react'
import { getTextSegments, highlightText, editCaretPosition } from '../utils'
const TextArea = ({ highlighted, ...rest }) => {
const editorRef = useRef()
const [lastHtml, setLastHtml] = useState()
//onInputChnage callback function
const handleInputChange = (_) => {
const editor = editorRef.current
const textSegments = getTextSegments(editor)
const textContent = textSegments.map(({ text }) => text).join('')
let html = highlightText(textContent, highlighted) // returns the text with highlighted words
setLastHtml(html) // sets the html of dangerouslySetInnerHTML
}
useEffect(() => {
const editor = editorRef.current
editCaretPosition(editor) //replaces the caret at the end of text
}, [lastHtml])
return (
<div
className="textarea"
contentEditable
placeholder="Enter your text here"
ref={editorRef}
onInput={handleInputChange}
spellCheck={false}
dangerouslySetInnerHTML={{ __html: lastHtml }}
{...rest}
/>
)
}
export default TextArea
If we run our application with the code above we can see that caret jumping behaviour:
Fix Jumping Caret
In order to fix the jumping caret problem, instead of using useEffect
hook we are going to use useLayoutEffect
and voilà there is no juming cursor problem.
Why? because dangerouslySetInnerHTML
is a DOM mutation and we need to update the caret palce immediately after React performs this DOM mutation in pther words we need to run our editCaretPlacement
helper function synchronously not asynchronously, and useEfect
is asynchronous whereas useLayoutEffect
is synchronous.
// change useEffect to useLayoutEffect
...
useLayoutEffect(() => {
const editor = editorRef.current;
editCaretPosition(editor);
}, [lastHtml]);
...
Conclusion
In this blog I've created a simple text editor component with <div contenteditable
. And I've shown how to handle the jumping cursor problem. I hope this blog could help you to understand the difference between useLayoutEffect
and useEffect
.
You can find the complete code for the custom textarea element that highlights the restricted words, made of <div contenteditable>
and with useLayoutEffect
can be found from 👉 here