Get text content from React element stored in a variable
Asked Answered
U

5

9

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

Upstate answered 28/7, 2020 at 19:4 Comment(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.Constance
https://mcmap.net/q/1171996/-extract-plain-text-from-react-object-duplicate this is a typescript version of one solution which I wrote using class componentsClownery
E
18

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.

Edict answered 30/7, 2020 at 12:57 Comment(5)
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('');Aurangzeb
Be happy with what you made, i'm using it without any complaintsOutdare
@André nice to know that React's internals apparently have not changed/broken this in the past two years.Edict
I implemented it yesterday, in a React 18 app so, looks like itOutdare
+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.Gregory
C
2

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

Colwen answered 10/5, 2022 at 11:23 Comment(0)
G
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));
  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>
}
Granite answered 13/11, 2023 at 22:34 Comment(0)
C
0

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);
Clownery answered 2/9 at 20:21 Comment(0)
U
-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.

Upstate answered 3/8, 2020 at 16:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.