10

Is there a way to get text content from a React element stored in a variable without ref?

There is a functional component, that receives title prop, which contains react element:

function component({ title }) {
 const content = title.textContent() // Need Something like this
}

and this title prop might have react node like: <div>Some Title</div>. But I'd like to get only content of the node, in a variable before rendering it. Is it possible?

When I console.log title variable this is the output, The content I want is inside props.children array, so is there a method to get it without traversing through keys:

enter image description here

2
  • Just check where the content is store by expanding the props in the console, then do title.props.children.attributeThatStoreTheText to get the value. Commented Jul 28, 2020 at 19:08
  • stackoverflow.com/a/78572057/1354940 this is a typescript version of one solution which I wrote using class components Commented Sep 2, 2024 at 20:23

6 Answers 6

21

I've not found a better solution than indeed traversing the object to get the text. In TypeScript:

/**
 * Traverse any props.children to get their combined text content.
 *
 * This does not add whitespace for readability: `<p>Hello <em>world</em>!</p>`
 * yields `Hello world!` as expected, but `<p>Hello</p><p>world</p>` returns
 * `Helloworld`, just like https://mdn.io/Node/textContent does.
 *
 * NOTE: This may be very dependent on the internals of React.
 */
function textContent(elem: React.ReactElement | string): string {
  if (!elem) {
    return '';
  }
  if (typeof elem === 'string') {
    return elem;
  }
  // Debugging for basic content shows that props.children, if any, is either a
  // ReactElement, or a string, or an Array with any combination. Like for
  // `<p>Hello <em>world</em>!</p>`:
  //
  //   $$typeof: Symbol(react.element)
  //   type: "p"
  //   props:
  //     children:
  //       - "Hello "
  //       - $$typeof: Symbol(react.element)
  //         type: "em"
  //         props:
  //           children: "world"
  //       - "!"
  const children = elem.props && elem.props.children;
  if (children instanceof Array) {
    return children.map(textContent).join('');
  }
  return textContent(children);
}

I don't like it at all, and hope there's a better solution.

Sign up to request clarification or add additional context in comments.

5 Comments

Just a quick note that this method doesn't handle arrays. This can be easily achieved by adding a 2nd check: if (Array.isArray(elem)) return elem.map((e) => getJSXTextContent(e)).join('');
Be happy with what you made, i'm using it without any complaints
@André nice to know that React's internals apparently have not changed/broken this in the past two years.
I implemented it yesterday, in a React 18 app so, looks like it
+1 for your answer, but your code doesn't work for me. I'm testing this on complex components - like predesigned progress-bar with label next to it, and other library controls. And I'm always getting empty string. However, for simple staff like basic divs and spans it's working fine.
3

use https://github.com/fernandopasik/react-children-utilities

import Children from 'react-children-utilities'


const MyComponent = ({ children }) => Children.onlyText(children)

from https://github.com/facebook/react/issues/9255

Comments

1

I have write this recursive function

 extractString(obj) {
  if (typeof obj === 'string') return obj;
  else if (React.isValidElement(obj)) {
    return this.extractString(obj.props.children);
  } else if (Array.isArray(obj)) {
    return obj.map(e => this.extractString(e)).join(' ');
  } else return obj.toString();
}

I'm using this for show error message at bottom of an input:

<input ref={.....} value={....} ..... />
<p>{this.props.errorMessage}</p>

BUUUUUT if the user still click on the submit button... I want to show the same text in the default browser error message without rewrite setting the same massage only once.

const errorMessage = this.extractString(this.props.errorMessage);
//this is the ref to the input
this.input.current.setCustomValidity(errorMessage);

Comments

0

I want to add to Arjan's answer, and allow supporting not only ReactElement, but the mode general ReactNode:

/**
 * Get text content from a React element.
 *
 * This does not add whitespace for readability: `<p>Hello <em>world</em>!</p>`
 * yields `Hello world!` as expected, but `<p>Hello</p><p>world</p>` returns
 * `Helloworld`, just like https://mdn.io/Node/textContent does.
 */
async function textContent (elem: React.ReactNode): Promise<string> {
  // type ReactNode = string | number | bigint | boolean
  //                | React.ReactElement<any, string | React.JSXElementConstructor<any>>
  //                | Iterable<React.ReactNode>
  //                | React.ReactPortal | Promise<...> | null | undefined
  if (elem === null || elem === undefined) {
    return ''
  }
  if (typeof elem === 'string' ||
      typeof elem === 'number' ||
      typeof elem === 'bigint' ||
      typeof elem === 'boolean') {
    return elem.toString()
  }
  if (elem instanceof Promise) {
    return textContent(await elem)
  }
  if ('props' in elem) {
    return textContent(elem.props.children as React.ReactNode)
  }
  // elem is `Iterable<React.ReactNode>`
  const array = Array.from(elem)
  return array.map(textContent).join('')
}

Comments

0

There is a newer (and seemingly better) way to do this now:

// typescript
const nodeToString = (node: ReactNode) => {
  const div = document.createElement("div");
  const root = createRoot(div);
  flushSync(() => root.render(node));
  root.unmount();
  return div.innerText; // or innerHTML or textContent
};

This is the recommended replacement for renderToString from react-dom/server. Can't comment on its performance vs. renderToString or the custom solutions in the other answers here, but this seems more robust.

One gotcha is React doesn't like it if you call flushSync within a render template or even a useEffect (see an example here of how flushSync is intended to be used), and you'll get lots of console errors.

Ideally, you'd put it in a callback that only runs as a result of a user action. But if you can't do that, here's an example of a work-around:

function someComponent({ children }) {
  const [label, setLabel] = useState("");

  useEffect(() => {
    // run outside of react lifecycle
    window.setTimeout(() => setLabel(nodeToString(children)));
  }, [content]);

  return <div aria-label={label}>{children}</div>
}

2 Comments

Won't this cause a memory leak if you don't clean up the div afterwards with div.remove() or something?
I've updated the answer to use root.unmount(). This should clean up anything that was mounted or used.
-1

Thanks @Arjan for the effort and solution, but I have changed something in the component, to get the title in string format. Now I have added another props to the component: renderTitle which is a function to render custom react title.

So now I am passing title as string:

<Component
  title="Some content"
  renderTitle={(title) => <div>{title}</div> }
/>

and inside component:

  <div>{renderTitle ? renderTitle(title) : title}</div>

With this implementation, I can use title as string to do what I want inside the component, while also supporting custom title render.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.