Rahul Sagore
Rahul Sagore

Reputation: 1668

Get text content from React element stored in a variable

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

Upvotes: 10

Views: 20575

Answers (6)

Yonatan Reicher
Yonatan Reicher

Reputation: 98

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('')
}

Upvotes: 0

jon_eldiablo
jon_eldiablo

Reputation: 380

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);

Upvotes: 0

V. Rubinetti
V. Rubinetti

Reputation: 1776

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>
}

Upvotes: 0

Moe Singh
Moe Singh

Reputation: 937

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

Upvotes: 3

Arjan
Arjan

Reputation: 23569

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.

Upvotes: 20

Rahul Sagore
Rahul Sagore

Reputation: 1668

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.

Upvotes: -1

Related Questions