Reputation: 1668
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:
Upvotes: 10
Views: 20575
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
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
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
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
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
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