Wrapping a component with itself in React? - javascript

Let's say I have a component named Match (renders span) and a string like Matched text to be wrapped with that component.
I'm expecting a final result like this:
<Match>Matched <Match>text</Match></Match>
On first wrap, my source text turns into a component like this:
<Match>Matched text</Match>
With this, is it possible wrap the inside of it (the text part) with another Match component? What should be the approach here?
Thanks.

What you need is ofcourse possible, each instance of the Match is different and whatever you write between Match tags are the children of the match component. So you just need
class Match extends React.Component {
render() {
return (
<span>{this.props.children}</span>
)
}
}
class App extends React.Component {
render() {
return (
<Match>Matched <Match>text</Match></Match>
);
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div id="app"></div>

It should work just right. Is it what you are looking for?
class Match extends React.Component {
render() {
if (this.props.children === 'text') {
return (
<span style={{ backgroundColor: 'yellow' }}>{this.props.children}</span>
)
} else {
return (
<span style={{ color: 'blue' }}>{this.props.children}</span>
)
}
}
}
class Test extends React.Component {
matchString(string) {
/* you can work with the raw string here if you want */
return string.split(' ')
.map(word => <Match>{word}</Match>);
/* inside the map you can do whatever you want here, maybe conditional wrapping, like if(word === 'text') <Match>word</Match> else word ... */
}
render() {
return (
<div>
{this.matchString('Matched text')}
</div>
);
}
}
ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body id="root">
</body>

Related

React.cloneElement not appending className

How can I append or change the className prop with React.cloneElement?
When I use React.cloneElement I'm unable to change or append the className prop. I've searched for hours but I found nothing. React.Children.only or removing the spread don't change the behavior. It appear to be a bug, or a performance optimization feature?.
Expect html: <div class="parent"><div class="child other-class">testing...</div></div>
Result html: <div class="parent"><div class="child">testing...</div></div>
Class example:
class Parent extends React.Component {
render() {
return (
<div className={"parent"}>
{React.cloneElement(React.Children.only(this.props.children), {
...this.props.children.props,
className: `${this.props.children.props.className} other-class`,
})}
</div>
);
}
}
class Child extends React.Component {
render() {
return <div className={"child"}>{"testing..."}</div>;
}
}
Functional component example:
const Parent = ({ children }) => (
<div className={"parent"}>
{React.cloneElement(React.Children.only(children), {
...children.props,
className: `${children.props.className} other-class`,
})}
</div>
);
const Child = () => <div className={"child"}>{"testing..."}</div>;
const Parent = ({ children }) => (
<div className={"parent"}>
{React.cloneElement(React.Children.only(children), {
...children.props,
className: `${children.props.className} other-class`,
})}
</div>
);
const Child = () => <div className={"child"}>{"testing2..."}</div>;
ReactDOM.render(
<React.StrictMode>
<Parent>
<Child />
</Parent>
</React.StrictMode>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The problem is that you're never using className in Child, which is what you're manipulating in Parent. Child puts a className on a div, but that isn't Child's className, it's just a hardcoded one that Child puts on the div.
If you want Child to put that class on the div, you have to write the code to do that. Also, you don't need the spread, the props are merged. Finally, to get the original className, I'd use the result of calling Children.only, rather than going back to this.props.children (though that will work because only would throw if there weren't only one).
See comments:
class Parent extends React.Component {
render() {
// Get the `className` from the child after verifying there's only one
const child = React.Children.only(this.props.children);
const className = `${child.props.className} other-class`;
return (
<div className={"parent"}>
{React.cloneElement(child, {
// No need to spread previous props here
className,
})}
</div>
);
}
}
class Child extends React.Component {
render() {
// Use `className` from `Child`'s props
const className = (this.props.className || "") + " child";
return <div className={className}>{"testing..."}</div>;
}
}
// Note the `classname` on `Child`, to show that your code using
// `this.props.children.props.className`
ReactDOM.render(<Parent><Child className="original"/></Parent>, document.getElementById("root"));
.child {
color: red;
}
.child.other-class {
color: green;
}
.original {
font-style: italic;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

Remove element from array with onClick and react

I'm very new to JS and React and coding in general. I'm trying to remove an item from an array using onClick, like so:
const { Component } = React;
class Board extends Component{
constructor(props) {
super(props);
this.state = {
comments: [
'I like Rosie',
'I like you',
'I like bacon',
'lalalala'
],
}
}
removeComment = (e) =>{
var filteredArray = this.state.comments.filter(item => item !== e.target.value)
this.setState({comments: filteredArray});
console.log(e.target.value)
}
eachComment = (text, i) => {
return ( <p id={i} key={i} > comment: {text} {i}</p> )
}
render(){
console.log('render');
return (
<div className="gif-list" onClick={this.removeComment}>
{this.state.comments.map(this.eachComment)}
</div>
);
}
}
ReactDOM.render(
<Board/>,
document.querySelector('#mount'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="mount"><div>
Not much is happening though. My console.log(e.target.value) comes back as undefined. What am I doing wrong?
I have tried looking at other answers but implementing (to me) abstract solutions to other similar problems is not working for me.
Thanks in advance, and apologies for such a simple question.
The div you have the event handler on does not have a value attribute. But even if it did I don't think it will do what you want.
You'll need to change your code a bit in order to get it working like you want:
class Board extends React.Component{
constructor(props) {
super(props);
this.state = {
comments: [
'I like Rosie',
'I like you',
'I like bacon',
'lalalala'
],
}
}
removeComment = (e) =>{
// Check the index in the filter to see if it should be kept or removed
var filteredArray = this.state.comments.filter((item, i) => i != e.target.id)
this.setState({comments: filteredArray});
console.log(e.target.id)
}
eachComment = (text, i) => {
// Move the event handler to be on each item
return ( <p id={i} key={i} onClick={this.removeComment}> comment: {text} {i}</p> )
}
render(){
return (
<div className="gif-list">
{this.state.comments.map(this.eachComment)}
</div>
);
}
}
ReactDOM.render(<Board />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
You'll notice the important changes are:
Give the remove handler to each mapped element
Use an attribute from the event target that actually exists and can be used to determine which element should be removed.
For this one you need to specifically pass in the param when you bind the onClick function
return (
<div className="gif-list" onClick={(e)=>this.removeComment(e)}>
{this.state.comments.map(this.eachComment)}
</div>
);

What is the correct way to select one of many child elements in React?

I have a small part of my new React app which contains a block of text, AllLines, split into line-by-line components called Line. I want to make it work so that when one line is clicked, it will be selected and editable and all other lines will appear as <p> elements. How can I best manage the state here such that only one of the lines is selected at any given time? The part I am struggling with is determining which Line element has been clicked in a way that the parent can change its state.
I know ways that I can make this work, but I'm relatively new to React and trying to get my head into 'thinking in React' by doing things properly so I'm keen to find out what is the best practice in this situation.
class AllLines extends Component {
state = {
selectedLine: 0,
lines: []
};
handleClick = (e) => {
console.log("click");
};
render() {
return (
<Container>
{
this.state.lines.map((subtitle, index) => {
if (index === this.state.selectedLine) {
return (
<div id={"text-line-" + index}>
<TranscriptionLine
lineContent={subtitle.text}
selected={true}
/>
</div>
)
}
return (
<div id={"text-line-" + index}>
<Line
lineContent={subtitle.text}
handleClick={this.handleClick}
/>
</div>
)
})
}
</Container>
);
}
}
class Line extends Component {
render() {
if (this.props.selected === true) {
return (
<input type="text" value={this.props.lineContent} />
)
}
return (
<p id={} onClick={this.props.handleClick}>{this.props.lineContent}</p>
);
}
}
In your case, there is no really simpler way. State of current selected Line is "above" line collection (parent), which is correct (for case where siblings need to know).
However, you could simplify your code a lot:
<Container>
{this.state.lines.map((subtitle, index) => (
<div id={"text-line-" + index}>
<Line
handleClick={this.handleClick}
lineContent={subtitle.text}
selected={index === this.state.selectedLine}
/>
</div>
))}
</Container>
and for Line component, it is good practice to use functional component, since it is stateless and even doesn't use any lifecycle method.
Edit: Added missing close bracket
'Thinking in React' you would want to give up your habit to grab DOM elements by their unique id ;)
From what I see, there're few parts missing from your codebase:
smart click handler that will keep only one line selected at a time
edit line handler that will stick to the callback that will modify line contents within parent state
preferably two separate components for the line capable of editing and line being actually edited as those behave in a different way and appear as different DOM elements
To wrap up the above, I'd slightly rephrase your code into the following:
const { Component } = React,
{ render } = ReactDOM
const linesData = Array.from(
{length:10},
(_,i) => `There goes the line number ${i}`
)
class Line extends Component {
render(){
return (
<p onClick={this.props.onSelect}>{this.props.lineContent}</p>
)
}
}
class TranscriptionLine extends Component {
constructor(props){
super(props)
this.state = {
content: this.props.lineContent
}
this.onEdit = this.onEdit.bind(this)
}
onEdit(value){
this.setState({content:value})
this.props.pushEditUp(value, this.props.lineIndex)
}
render(){
return (
<input
style={{width:200}}
value={this.state.content}
onChange={({target:{value}}) => this.onEdit(value)}
/>
)
}
}
class AllLines extends Component {
constructor (props) {
super(props)
this.state = {
selectedLine: null,
lines: this.props.lines
}
this.handleSelect = this.handleSelect.bind(this)
this.handleEdit = this.handleEdit.bind(this)
}
handleSelect(idx){
this.setState({selectedLine:idx})
}
handleEdit(newLineValue, lineIdx){
const linesShallowCopy = [...this.state.lines]
linesShallowCopy.splice(lineIdx,1,newLineValue)
this.setState({
lines: linesShallowCopy
})
}
render() {
return (
<div>
{
this.state.lines.map((text, index) => {
if(index === this.state.selectedLine) {
return (
<TranscriptionLine
lineContent={text}
lineIndex={index}
pushEditUp={this.handleEdit}
/>
)
}
else
return (
<Line
lineContent={text}
lineIndex={index}
onSelect={() => this.handleSelect(index)}
/>
)
})
}
</div>
)
}
}
render (
<AllLines lines={linesData} />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

React HOC with multiple components

I want to create a React HOC that would ideally receive two components instead of one wrapped component and toggle between them. That is, in the code below, instead of <h3>component one</h3> and <h3>component two<h3>, they would each represent child components. How would I be able to accomplish this? Some psuedo code for how I would write this HOC:
<HOC>
<ComponentOne />
<ComponentTwo />
</HOC>
<HOC
componentOne={<ComponentOne />}
componentTwo={<ComponentTwo />}
/>
hoc(componentOne, componentTwo)
class HOC extends React.Component {
constructor() {
super();
this.state = {
onClick: false,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({onClick: !this.state.onClick});
}
render() {
return (
<div>
<button onClick={this.handleClick}>Click Me!</button>
{
this.state.onClick ?
<h3>component one</h3> :
<h3>component two</h3>
}
</div>
);
}
}
ReactDOM.render(<HOC />, app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I am not sure if I understood you. Why do you need it to be HOC?
If you would pass components as props like that:
<HOC
componentOne={<ComponentOne />}
componentTwo={<ComponentTwo />}
/>
Then you would be able to access them using props.
render() {
return (
<div>
<button onClick={this.handleClick}>Click Me!</button>
{
this.state.onClick ?
this.props.componentOne :
this.props.componentTwo
}
</div>
);
}
If a component has more than one child then this.props.children will be an array.
class HOC extends React.Component {
// ... rest of code ....
render() {
const { onClick } = this.state;
const { children } = this.props;
return !onClick ? children[0] : children[1];
}
}
Then use it like so:
<HOC>
<div>Child One</div>
<div>Child Two</div>
</HOC>
Obviously this will only work with two children but you could extend it by passing an integer to <HOC> through props to tell it what child to select.
Edit
After a quick look at the docs this is a better version of what I wrote above as this.props.children is not an array, it is an opaque data structure:
class HOC extends React.Component {
// ... rest of code ...
render() {
const { onClick } = this.state;
const children = React.Children.toArray(this.props.children);
return !onClick ? children[0] : children[1];
}
}

Return multiple React elements in a method without a wrapper element

I'm trying to return multiple React elements from a helper method. I could solve it simply by moving around some code, but I'm wondering if there's a cleaner way to solve it. I have a method that returns part of the render method, and that functions needs to return both a React element and some text. It's clearer through an example:
class Foo extends React.Component {
_renderAuthor() {
if (!this.props.author) {
return null;
}
return [
' by ',
<a href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
]; // Triggers warning: Each child in an array or iterator should have a unique "key" prop.
render() {
return (
<div>
{this.props.title}
{this._renderAuthor()}
</div>
);
}
}
I know the render method has to return exactly 1 React element. Using a helper method like this would trigger a warning, and fixing the warning (by adding keys) would make the code too convoluted. Is there a clean way to do this without triggering a warning?
Edit:
Another use case:
render() {
return (
<div>
{user
? <h2>{user.name}</h2>
<p>{user.info}</p>
: <p>User not found</p>}
</div>
);
}
Edit 2:
Turns out this isn't possible yet, I wrote about 2 workarounds here: https://www.wptutor.io/web/js/react-multiple-elements-without-wrapper
Support has been added using the Fragment component. This is a first-class component.
So you can now use:
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
For more information visit: https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html
The error message tells you exactly how to solve this:
Each child in an array or iterator should have a unique "key" prop.
Instead of this:
return [
' by ',
<a href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
];
Do this:
return [
<span key="by"> by </span>,
<a key="author" href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
];
Yes, you need to wrap the text node ("by") in a span in order to give it a key. Such are the breaks. As you can see, I've just given each element a static key, since there's nothing dynamic about them. You could just as well use key="1" and key="2" if you wanted.
Alternatively, you could do this:
return <span> by <a href={getAuthorUrl(this.props.author)}>{this.props.author}</a></span>;
...which obviates the need for keys.
Here's the former solution in a working snippet:
const getAuthorUrl = author => `/${author.toLowerCase()}`;
class Foo extends React.Component {
_renderAuthor() {
if (!this.props.author) {
return null;
}
return [
<span key="by"> by </span>,
<a key="author" href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
];
}
render() {
return (
<div>
{this.props.datePosted}
{this._renderAuthor()}
</div>
);
}
}
ReactDOM.render(<Foo datePosted="Today" author="Me"/>, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
It's not currently possible to do this without some sort of workaround like wrapping everything in another component, since it ends up with the underlying React code trying to return multiple objects.
See this active Github issue where support for this is being considered for a future version though.
Edit: You can now do this with Fragments in React 16, see:
https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html
There is another way to solve this. I will suggest you create another component Author.js:
function Author(props) {
return (<span>
<span> by </span>
<a href={props.getAuthorUrl(props.author)}>{props.author}</a>
</span>)
}
class Foo extends React.Component {
render() {
return (
<div>
{this.props.title}
{this.props.author && <Author author={this.props.author} getAuthorUrl={this.getAuthorUrl} />}
</div>
);
}
}
I didn't test this code though. But it will look more cleaner I think. Hope it helps.
I like to have an If-component around for such things, and I have wrapped everything into a span, as it doesn't really break anything and makes the need for keys go away...
const getAuthorUrl = author => `/${author.toLowerCase()}`;
function If({condition,children}) {
return condition ? React.Children.only(children) : null;
}
class Foo extends React.Component {
render() {
return (
<div>
{this.props.datePosted}
<If condition={this.props.author}>
<span> by
<a key="author" href={getAuthorUrl(this.props.author)}>
{this.props.author}
</a>
</span>
</If>
</div>
);
}
}
ReactDOM.render(<Foo datePosted="Today" author="Me"/>, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
...skipping the array thing altogether?
This is a bit hacky but it doesn't have unnecessary jsx as you wished.
var author = 'Daniel';
var title = 'Hello';
var Hello = React.createClass({
_renderAutho0r: function() {
if (!author) {
return null;
}
return {author}
},
render: function() {
var by = author ? ' by ' : null;
return (
<div>
{title}
{by}
{this._renderAutho0r()}
</div>
);
}
});
React.render(<Hello name="World" />, document.body);
my JSFiddle
You can return fragments from sub-rendering functions but not from the main render function, at least before React 16. In order to do so, return an array of components. You don't need to set keys manually unless your fragment children will change (arrays are keyed with indices by default).
For creating fragments you may also use createFragment.
For inline usage, you may use an array or leverage immediately invoked arrow function.
See the example below:
const getAuthorUrl = author => `/${author.toLowerCase()}`;
class Foo extends React.Component {
constructor() {
super();
this._renderAuthor = this._renderAuthor.bind(this);
this._renderUser = this._renderUser.bind(this);
}
_renderAuthor() {
if (!this.props.author) {
return null;
}
return [
' by ',
<a href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
];
}
_renderUser() {
return [
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
]
}
render() {
return (
<div>
{this.props.datePosted}
{this._renderAuthor()}
<div>
{this.props.user
? this._renderUser()
: <p>User not found</p>}
</div>
<div>
{this.props.user
? [
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
]
: <p>User not found</p>}
</div>
<div>
{this.props.user
? (() => [
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
])()
: <p>User not found</p>}
</div>
</div>
);
}
}
ReactDOM.render(<Foo datePosted="Today" author="Me" user={{name: 'test', info: 'info'}} />, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
In order to not get warnings each child must be assigned a key. In order to do so, instead of returning an array please use helper function fragment(...children) to assign index-based keys automatically. Please note that strings must be converted to spans or other nodes that can be assigned with a key:
const fragment = (...children) =>
children.map((child, index) =>
React.cloneElement(
typeof child === 'string'
? <span>{child}</span>
: child
, { key: index }
)
)
const getAuthorUrl = author => `/${author.toLowerCase()}`;
const fragment = (...children) =>
children.map((child, index) =>
React.cloneElement(
typeof child === 'string'
? <span>{child}</span>
: child
, { key: index }
)
)
class Foo extends React.Component {
constructor() {
super();
this._renderAuthor = this._renderAuthor.bind(this);
this._renderUser = this._renderUser.bind(this);
}
_renderAuthor() {
if (!this.props.author) {
return null;
}
return fragment(
' by ',
<a href={getAuthorUrl(this.props.author)}>{this.props.author}</a>
);
}
_renderUser() {
return fragment(
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
)
}
render() {
return (
<div>
{this.props.datePosted}
{this._renderAuthor()}
<div>
{this.props.user
? this._renderUser()
: <p>User not found</p>}
</div>
<div>
{this.props.user
? fragment(
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
)
: <p>User not found</p>}
</div>
<div>
{this.props.user
? (() => fragment(
<h2>{this.props.user.name}</h2>,
<p>{this.props.user.info}</p>
))()
: <p>User not found</p>}
</div>
</div>
);
}
}
ReactDOM.render(<Foo datePosted="Today" author="Me" user={{name: 'test', info: 'info'}} />, document.getElementById('container'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="container"></div>
Try this:
class Foo extends React.Component {
_renderAuthor() {
return <a href={getAuthorUrl(this.props.author)}>{this.props.author}</a>
}
render() {
return (
<div>
{this.props.title}
{this.props.author && " by "}
{this.props.author && this._renderAuthor()}
</div>
);
}
}
Perhaps a more simple way would be to rethink how you're architecting your application. However, in a more simple way.
You're triggering the warning because you're trying to render from an array and not react elements but directly html. In order to approach this, you would have to do
{this._renderAuthor().map(
(k,i) => (React.addons.createFragment({k}))
) }
React addons createFragment function basically does that, it reduces your html elements into react fragments that you can render.
React createFragment documentation
Alternatively, in a much better approach, you can create an AuthorLink stateless component like this..
function AuthorLink(props) {
return (
<div className="author-link">
<span> by </span>
<a href={props.authorUrl}> {props.author} </a>
</div>
});
}
and use this in your main component's render
render() {
const { author } = this.props;
return (
<div>
{this.props.datePosted}
<AuthorLink url={getAuthorUrl(author)} author={author} />
</div>
);
}
Try this approach on your array:
return [
<span key={'prefix-'+random_string_generator()}>' by '</span>,
<a key={'prefix-'+random_string_generator()} href={getAuthorUrl(this.props.author)}>{this.props.author}</a>,
];

Categories