DEV Community

Cover image for Understanding React's Declarative UI Through a Copy Button Example
Samit Kapoor
Samit Kapoor

Posted on

Understanding React's Declarative UI Through a Copy Button Example

I was building a simple Copy button. The kind that copies some text to the clipboard and briefly shows a “Copied!” message for 1 second before reverting to its original label.

I already knew how to implement it the “React way” using useState to manage the button’s text. But out of curiosity, I decided to try a more direct approach using vanilla JavaScript’s DOM manipulation by changing innerText manually.

That small decision led me down an unexpected path and helped me better understand React’s declarative nature. Let’s explore what happened and why it’s an important lesson for anyone working with React.

The Imperative Attempt

Here's what I tried doing. Instead of relying on React state, I tried change the value of innerText inside the onClick handler.

<button
  onClick={async (e) => {
    navigator.clipboard.writeText(text);
    e.currentTarget.innerText = 'Copied!';
    await new Promise((resolve) => setTimeout(resolve, 1000));
    e.currentTarget.innerText = text;
  }}
>
  {text}
</button>

Enter fullscreen mode Exit fullscreen mode

This seems like it should work. When the button is clicked:

  • The text gets copied to the clipboard.
  • I change the innerText value of the button to 'Copied!'.
  • After a second the innerText value of the button is changed back to what it was.

But in reality button's innerText value changed to 'Copied!' but it never changed back to text value.

Why it doesn't work?

It didn't work simply because React didn't know that I manipulated the DOM.

The issue lies in React's declarative rendering model. The UI is a reflection of component's state and props. When you write:

<button>{text}</button>
Enter fullscreen mode Exit fullscreen mode

You're telling React "The content of this button should always be whatever the value of text is".

React uses virtual DOM to keep track of what UI should look like and compare it with the real DOM to figure out what needs to be changed.

So when I tried to change the value of innerText back to text after 1 second... it failed because React had already re-rendered that component in between due to stricter mode in development. By the time the timeout ran DOM node has already been replaced and that's why it gets stucked to 'Copied!' only.

Vanilla JS DOM manipulation in React is an anti pattern and this is why it isn't advised, they may get silently ignored or break when DOM is rebuilt.

Using useState

Instead of manually updating the DOM, we use useState to register updates.

import { useState } from 'react';

const CopyButton = ({ text }) => {
  const [displayText, setDisplayText] = useState(text);

  const handleClick = async () => {
    await navigator.clipboard.writeText(text);
    setDisplayText('Copied!');
    setTimeout(() => setDisplayText(text), 1000);
  };

  return <button onClick={handleClick}>{displayText}</button>;
}

export default CopyButton
Enter fullscreen mode Exit fullscreen mode

The button's text is controlled by displayText, a React state variable. When the button is clicked:

  • We write to the clipboard.
  • We update the state to 'Copied!', triggering a React re-render.
  • After 1 second, we reset the state back to the original text, triggering another re-render.

React uses the Virtual DOM to efficiently update the UI with each state change.

Since React is in charge of rendering, the UI always stays in sync with the component’s logic, no surprises, no manual innerText issues, and no bugs due to stale DOM nodes.


Hi, I'm Samit. A Software Developer and a freelancer who’s always on the lookout for exciting, real world projects to build and contribute to. I love hearing from people, whether it’s to collaborate, share ideas, or work together.

If you're looking to hire a passionate developer or even if you just want to say hi, feel free to check out my portfolio and reach out. I'd love to connect!

Top comments (0)