Reputation: 605
I'm working on a React with TypeScript application. I'm using a necessary library that has a component that takes a text property and renders it as an SVG. We'll call this library component <LibraryRenderer />
.
I've wrapped this component in another component, <Renderer />
, that passes the children (as text) into the library component like so:
function Renderer(props: { children: string }) {
return <LibraryRenderer text={props.children} />;
}
Now, what I'd like is to create a component that transforms the text inside of it into another string and use in the <Renderer />
component. For instance, this new component <Wrap />
could surround existing text with "BEGIN"
and "END"
respectively. Here's something like what I'd want the code to look like:
function Wrap(props: { children: string }) {
return `BEGIN ${props.children} END`;
}
Then, you'd be able to use these components in a hierarchy like this:
<Renderer>
<Wrap>
<Wrap>
Some Inner Text
</Wrap>
</Wrap>
</Renderer>
And I would want this to reduce to:
<LibraryRenderer text={"BEGIN BEGIN Some Inner Text END END"} />
I could write the text-transformation components as regular ol' functions but it seems like that would remove some of the nice flexibility of JSX syntax. Also, if I ever had props or state inside any of these text-transformation components, I wouldn't be able to use React to manage them.
function Wrap(value: string) {
return `BEGIN ${props.children} END`;
}
<Renderer>
{Wrap(
Wrap(
"Some Inner Text"
)
)}
</Renderer>
I feel like this might not exactly be the correct usage of React but I think that depends on if its possible and whether the code is "hacky" or not. Any help for how I should move forward would be greatly appreciated!
Upvotes: 1
Views: 1987
Reputation: 8990
It's possible to generate the text with a Wrap
component.
The markup in your app render method will be like following:
return (
<Renderer>
<Wrap>
<Wrap>Hello world</Wrap>
</Wrap>
</Renderer>
);
This will generate the text BEGIN BEGIN Hello world END END
inside of the Renderer
component.
For the Wrap
component we're using a span
-tag to wrap our text with the following code:
const Wrap: FunctionComponent = ({ children }) => {
return <span style={{ border: "1px solid red" }}>BEGIN {children} END</span>;
};
The Renderer
component is getting the text with components as children with the following code (LibrarayRenderer
will be explained later):
const Renderer: FunctionComponent = ({ children }) => {
const [finalText, setText] = useState("");
return (
<div>
<LibraryRenderer text={finalText} input={children} setText={setText} />
<p>Generated text: {finalText}</p>
</div>
);
};
As you can see, we're creating a state variable finalText
for our text that will be generated by our LibraryRenderer
component.
The input
prop are the children with the wrapped text and setText
is passed so we can update the finalText
inside of the renderer.
The finalText
is also passed to the LibraryRender
as this is needed there later (Note: It will be available on the second render because at the first render it's not ready.)
Now, we're ready to generate the LibraryRenderer
that will create the text for us.
type LibraryRendererProps = {
text: string;
input: ReactNode;
setText: Dispatch<SetStateAction<string>>;
};
const LibraryRenderer: FunctionComponent<LibraryRendererProps> = ({
text,
input,
setText
}) => {
const ref = useRef<HTMLHeadingElement>(null);
useEffect(() => {
setText(ref?.current?.innerText || "");
}, [ref, setText]);
console.log("text", text); // needed for renderer as string
// hiding the h1 with css because we're just using it to generate the text
return (
<h1 ref={ref} style={{ display: "none" }}>
{input}
</h1>
);
};
We're returning a heading (any other inline element will work) with display: "none"
style and a reference. The CSS is just used so it won't be displayed in the DOM. The reference is needed to get the text later. The {input}
will render our children with the wrapped text.
Now, we have to do a useEffect
with the ref
as array parameter - so this useEffect
will run if the reference changes. Inside of it, we're setting the finalText
to the innerText
of our rendered HTML by using our setText
method.
The HTML in DOM looks like:
<h1 style="display: none;">
<span style="border: 1px solid red;">BEGIN <span style="border: 1px solid red;">BEGIN Hello world END</span> END</span>
</h1>
With innerText
we're getting every text inside the h1
-tag. The border style on the span
is just for debugging (if display: none
is removed).
So we're now having the text BEGIN BEGIN Hello world END END
.
I've used FunctionComponent for the type of our components, so children
prop is defined there. You may have seen SFC
type in other examples but this is deprecated because React function components are no longer considered stateless - use FunctionComponent
instead.
If you'd like to add more types you can add them to the FunctionComponent
as a generic type like in the above snippet with FunctionComponent<LibraryRendererProps>
The LibraryRendererProps
type:
input: ReactNode;
same typing as children typestext: string;
our generated text as string
setText: Dispatch<SetStateAction<string>>;
uses React.Dispatch
and SetStateAction
type with setState
type as stringYou can find the complete code below (with-out typing) or in the following Codesandbox with Typescript.
const { useState, useRef, useEffect } = React;
const LibraryRenderer = ({ text, input, setText }) => {
const ref = useRef(null);
useEffect(() => {
setText(ref.current.innerText || "");
}, [ref, setText]);
console.log("text", text); // needed for renderer as string
// hiding the h1 with css because we're just using it to generate the text
return (
<h1
ref={ref}
style={{
display: "none",
}}
>{input}
</h1>
);
};
const Renderer = ({ children }) => {
const [finalText, setText] = useState("");
return (
<div>
<LibraryRenderer text={finalText} input={children} setText={setText} />
<p>Generated text: {finalText}</p>
</div>
);
};
const Wrap = ({ children }) => {
return (
<span
style={{
border: "1px solid red",
}}
> BEGIN {children} END
</span>
);
};
function App() {
return (
<div className="App">
<Renderer>
<Wrap>
<Wrap>Hello world</Wrap>
</Wrap>
</Renderer>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
body {
font-family: Arial, sans-serif;
}
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
Upvotes: 1
Reputation:
React is used to build the UI, but in your case you are operating on string, so using pure functions totally make sense.
In the last example, I will do something like
const generatedText = wrap(wrap("Some Inner Text"))
return <Renderer>{generatedText}</Renderer>
Upvotes: 0