Which is the best way to use React child components - javascript

In particular, I'm curious about the way to pass information along. In another thread, I had child components and the methods of passing certain props explained to me, as well as the dangers of <MyComponent children={...} />, and I'm curious which would be better: storing and working with the tabs as a mapped array of objects as in the example, or following the
<TabList> <Tab /><Tab /> </TabList> style. Is <MyComponent children={...} /> the same as <TabList tabs={this.state.tabs} />? I assume so, but apparently children as a prop is special case?
const tabs = [
{
id: 0,
label: "Archery",
content: "Lorem Ipsum 1"
},
{
id: 1,
label: "Baseball",
content: "Lorem Ipsum 2"
}
];
function TabContent(props) {
return (
<div className="tabContent">
{props.content}
</div>
);
}
class Tab extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(el) {
this.props.handleClick(el.target)
}
render() {
let active = (this.props.id === this.props.activeTab) ? "active" : ""
return (
<li id={this.props.id} onClick={this.onClick} className={active}>
{this.props.label}
</li>
);
}
}
class TabList extends React.Component {
constructor(props) {
super(props);
}
componentDidUpdate() {
$(this.refs.tabList).animate({scrollLeft: this.props.scrollPosition}, 400)
}
render() {
let tabList = this.props.tabs.map((tab) => {
return (
<Tab
key={tab.id}
id={tab.id}
activeTab={this.props.activeTab}
label={tab.label}
handleClick={this.props.handleClick}
/>
);
});
return (
<ul className="tabList" ref="tabList">
{tabList}
</ul>
);
}
}
class TabScroller extends React.Component {
render() {
return (
<div className="tabScroller">
<div className="NavList">
<TabNav handleClick={this.handleNavClick} />
<TabList
tabs={this.state.tabs}
activeTab={this.state.activeTab}
scrollPosition={this.state.scrollPosition}
handleClick={this.handleTabClick}
/>
</div>
<TabContent content={this.state.tabs[this.state.activeTab].content} />
</div>
);
}
}
// ========================================
ReactDOM.render(
<TabScroller />,
document.getElementById('root')
);

The usage of <TabList> <Tab /><Tab /> </TabList> is just a syntax sugar that JSX has brought. Internally, it's the same as <TabList children={[<Tab />, <Tab />]} />.

As it turns out, anything within <TabList></TabList> will be passed to the TabList component as props.children which includes all functions and props that might be added to child Components. This, among other reasons, is why it's more often best to pass the children as child Components rather than props and avoid having to unnecessarily duplicate props.
I have no single resource to officially confirm this, just experience gained through playing around in React.js, so if anyone can comment or DM me a link, I'll add it to the answer.

Related

How to dynamically render a nested component in React?

I want to render a child element based on the state in its parent. I tried to do the following (simplified version of the code):
class DeviceInfo extends Component {
constructor(props) {
super(props);
this.state = {
currentTab: "General",
};
this.tabsMap = {
General:
<React.Fragment>
<GeneralCard
id={this.props.id}
/>
</React.Fragment>
}
navToggle(tab) {
this.setState({ currentTab: tab });
}
this.tabsMap = {
General:
<React.Fragment>
<GeneralCard
id={this.props.id}
/>
</React.Fragment>
};
render() {
return (
<React.Fragment>
<div className="container">
<Nav className="nav-tabs ">
<NavItem>
<NavLink
className={this.state.currentTab === "General" ? "active" : ""}
onClick={() => {
this.navToggle("General");
}}
>
General
</NavLink>
</div>
{ this.tabsMap[this.state.currentTab] }
</React.Fragment>
);
}
}
But it did not work properly. Only when I put the contents of the tabsMap straight in the render function body it works (i.e. as a react element rather then accessing it through the object). What am I missing here?
Instead of making tabsMap an attribute which is only set when the component is constructed, make a method that returns the object, and call it from render:
getTabsMap() {
return {
General:
<React.Fragment>
<GeneralCard
id={this.props.id}
/>
</React.Fragment>
}
};
render() {
...
{ this.getTabsMap()[this.state.currentTab] }
...
}
You defining instance property with this.tabsMap (should be syntax error):
export default class App extends React.Component {
tabsMap = { General: <div>Hello</div> };
// Syntax error
// this.tabsMap = { General: <div>World</div> };
render() {
// depends on props
const tabsMapObj = {
General: <div>Hello with some props {this.props.someProp}</div>
};
return (
<FlexBox>
{this.tabsMap['General']}
{tabsMapObj['General']}
</FlexBox>
);
}
}
Edit after providing code:
Fix the bug in the constructor (Note, don't use constructor, it's error-prone, use class variables).
Moreover, remember that constructor runs once before the component mount if you want your component to be synchronized when properties are changed, move it to render function (or make a function like proposed).
class DeviceInfo extends Component {
constructor(props) {
...
// this.props.id not defined in this point
this.tabsMap = {
General:
<React.Fragment>
<GeneralCard
id={props.id}
/>
</React.Fragment>
}
render() {
// make a function to change the id
this.tabsMap = {
General:
<React.Fragment>
<GeneralCard
id={this.props.id}
/>
</React.Fragment>
};
return (
<>
{ this.tabsMap[this.state.currentTab] }
</>
);
}
}
I think it's a this binding issue. Not sure if your tabsMap constant should have this in front of it.
Alternative answer... you can inline the expression directly in the render as
{ this.state.currentTab === 'General' && <GeneralCard id={this.props.id} /> }

Understanding nesting vs parent in ReactJS

I have a class called: QuestionList, which creates Questions children, along with (nested) Alternatives:
QuestionList render:
<Question wording="wording...">
<Alternative letter="a" text="bla ..." />
<Alternative letter="b" text="ble ..." />
<Alternative letter="c" text="bli ..." />
<Alternative letter="d" text="blo ..." />
</Question>
Who is "alternatives" parent? Question (because it is nested) or QuestionList (because it created)?
How can pass a Question event handler to Alternative?
If I use
<Alternative onClick={this.handleClick} (...) />
It will pass QuestionList's handler (and not Question's handler - the desired behavior).
QuestionList
import React, { Component } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import Loader from 'react-loaders';
import Question from './Question';
import Alternative from './Alternative';
export default class QuestionList extends Component {
constructor(props) {
super(props);
this.state = {
questions: []
};
}
loadItems(page) {
let questions = this.state.questions;
axios.get('https://jsonplaceholder.typicode.com/photos?_start='+ page * 5 +'&_limit=5')
.then(response => {
console.log(response.data);
response.data.map(p => {
questions.push(p);
});
this.setState({questions});
});
}
handleClick() {
alert("QuestionList");
}
render() {
let items = [];
const loader = <Loader type="ball-scale-multiple" />;
this.state.questions.map((p, i) => {
items.push(
<Question
title={p.title}
key={i}
id={p.id}
>
<Alternative onClick={this.props.handleClick} key={1} text={ p.title } letter="a" />
<Alternative onClick={this.props.handleClick} key={2} text={ p.title } letter="b" />
<Alternative onClick={this.props.handleClick} key={3} text={ p.title } letter="c" />
<Alternative onClick={this.props.handleClick} key={4} text={ p.title } letter="d" />
<Alternative onClick={this.props.handleClick} key={5} text={ p.title } letter="e" />
</Question>
)
});
return (
<InfiniteScroll
key={1}
pageStart={0}
loadMore={this.loadItems.bind(this)}
hasMore={true}
loader={loader}
>
<div className="justify-content-center" id="react-app-questions-list">
{items}
</div>
</InfiniteScroll>
);
}
}
Question
import React, { Component } from 'react';
export default class Question extends Component {
constructor(props) {
super(props);
this.state = {
answer_class: "unanswered"
};
}
handleClick(isCorrect, e) {
// alert(this.props.id + ": " + isCorrect);
alert("Question");
}
render() {
return (
<div className={"list-group list-group-bordered mb-3 " + this.state.answer_class}>
<div className="list-group-item">
<div className="list-group-item-body">
<h4 className="list-group-item-title">
{ this.props.title }
</h4>
</div>
</div>
{ this.props.children }
</div>
);
}
}
Alternative
import React, { Component } from 'react';
class Alternative extends Component {
render() {
return (
<a className="list-group-item list-group-item-action react-app-alternative">
<div className="list-group-item-figure">
<div className="tile tile-circle bg-primary">{ this.props.letter }</div>
</div>
<div className="list-group-item-body"> { this.props.text }</div>
</a>
);
}
}
export default Alternative;
Who is Alternatives parent? Question (because it is nested) or QuestionList (because it created)?
Alternative parent is Question. If you check Question.props.children array (remember that Question is just an object), you will see Alternative types there.
function Question({ children }) {
console.log(children); // children[0].type === Alternative
return children;
}
Read more about React elements as objects here.
How can pass a Question event handler to Alternative?
You can inject props to Question children, for example:
function Question({ children }) {
console.log(children);
const injectChildren = React.Children.map(children, child =>
React.cloneElement(child, { letter: `${child.props.letter}-injected` })
);
return injectChildren;
}
For this you need to read about React Top-Level API and refer to React.Children API and cloneElement().
Check out the example:
Handlers are intended to work on context ... where state is managed .. then in <QuestionList/>. Prepare specific, parametrized handlers and use them to update common state.
Chaining 'desired' (more granular or more specific) handlers to pass values through the structure can't be practical. It won't be efficient, either.
Take a look at data/state flow in 'Formik` project - form, validations, fields. It can be a good source of inspiration for this problem.
<Question/> and <Alternative/> should be stateless, functional components - you don't need them to be statefull. KISS, YAGNI...

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

React: Use props or array of these

Could you tell me please. How can I make React component which I can use with props or with array of these props.
For example I have this component:
import React, { Component } from 'react'
export default class Links extends Component {
render () {
return (
<a
href={ this.props.link }
>
{ this.props.name }
</a>
)
}
}
And I want to use this component here:
import React, { Component } from 'react'
import Links from './Links'
export default class Block extends Component {
render () {
const social = [{
name: 'Twitter',
link: 'https://twitter.com',
}, {
name: 'FaceBook',
link: 'https://fb.com',
}]
return (
<div>
<div>
<Links someword={ social }>
</div>
<Links name={ 'Google' } link={ 'https://google.com' }>
</div>
)
}
}
Loop through your social array and map each value to your Links component and then put it in the render function of your Block component.
export default class Block extends Component {
render () {
const social = [{
name: 'Twitter',
link: 'https://twitter.com',
}, {
name: 'FaceBook',
link: 'https://fb.com',
}]
const linkComps = social.map(e =>
<Links name={ e.name } link={e.link} key={e.name} />;
);
return (
<div>
<div>
{ linkComps }
</div>
<Links name={ 'Google' } link={ 'https://google.com' }>
</div>
)
}
}
You can use map to iterate the array and create the Links component. Since your social variable is having the const value, so instead of defining that inside render method, define it outside in the starting of the file.
Write it like this:
const social = [{
name: 'Twitter',
link: 'https://twitter.com',
}, {
name: 'FaceBook',
link: 'https://fb.com',
}
]
class Block extends React.Component {
render () {
return (
<div>
<div>
{
social.map((el,i) => <Links
key={i}
name={el.name}
link={el.link} />)
}
</div>
<Links name={ 'Google' } link={ 'https://google.com' }/>
</div>
)
}
}
class Links extends React.Component {
render () {
return (
<a
href={ this.props.link }
>
{ this.props.name }
</a>
)
}
}
ReactDOM.render(<Block/>, document.getElementById('app'))
<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='app'/>
If you want to handle that inside Links component, you can write it like this, You can pass either an array by name array or pass the individual value by name and Link.
export default class Links extends Component {
_renderLinks(){
if(this.props.array && Array.isArray(this.props.array)){
return this.props.array.map((el,i) => <a
key={i}
href={ el.link}
>
{el.name}
</a>
}else{
return <a href={ this.props.link}> {this.props.name} </a>
}
}
render () {
return (
<div>
{this._renderLinks()}
</div>
)
}
}
The usual way to render multiple components from an array of props is with map:
render() {
// ...
return (
<div>
{social.map(props => (
<Link key={props.link} {...props}/>
))}
</div>
);
}
That said, having a single component that takes two different kinds of props is, generally speaking, a bad idea. That is to say, a component should take e.g. name and link props or it should take an array of objects with those properties. It should not do both.
A clean way to solve your problem is to have two components: A <Link> component that takes name and link props and renders a single link, and a <Links> (plural) component that takes an array of objects with those properties and renders a <Link> (singular) component for each one.
A basic implementation looks like the below. Click on ▸⃝ Run code snippet below to see it in action (note that I added some CSS just to show the boundaries of each component).
const Link = ({name, link}) => (
<a href={link}>{name}</a>
);
const Links = ({links}) => (
<div>
{links.map(props => <Link key={props.link} {...props}/>)}
</div>
);
class Block extends React.Component {
render() {
return (
<div>
{/* Render an array of links with <Links> */}
<Links links={this.props.social}/>
{/* Render a single link with <Link> */}
<Link name="Google" link="https://google.com"/>
</div>
);
}
}
const social = [
{ name: 'Twitter',
link: 'https://twitter.com',
},
{ name: 'Facebook',
link: 'https://facebook.com',
}
];
ReactDOM.render(<Block social={social}/>, document.querySelector('div'));
a {display: block;}
div div {border: 1px dotted gray; padding: 5px; margin-bottom: 5px;}
<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></div>

pass id through on click react.js

Below is my partial code but my question is very simple, how can I get says data-id="1" to my function when user clicked on the li?
render(){
return(
<ul id="todo">
{this.state.items.map((item,i) =>
<li className='list-group-item' key={i} data-id={item.id}>{item.name}
<button onClick={//how to pass item.id to my function?}>X</button>
</li>
)}
</ul>
)
}
Since you are already using ES6 - might be a little cleaner to use an arrow function here:
render(){
return(
<ul id="todo">
{this.state.items.map((item,i) =>
<li className='list-group-item' key={i} data-id={item.id}>{item.name}
<button onClick={() => this.yourfunc(item.id)}>X</button>
</li>
)}
</ul>
)
}
You can use bind() to do this.
render(){
return(
<ul id="todo">
{this.state.items.map((item,i) =>
<li className='list-group-item' key={i} data-id={item.id}>{item.name}
<button onClick={yourfunc.bind(this, item.id)}>X</button>
</li>
)}
</ul>
)
}
Your function will receive item.id as the first parameter
In my opinion, you shouldn't declare functions, nor bind methods within render method. Neither of these:
onClick={(e) => this.handleClick(e, item.id)}
onClick={this.handleClick.bind(this, item.id)}
I know it's plenty of tutoriales showing that syntax. But there's also a considerable number of blog posts warning about why that's not a good idea. Basically, you are creating a new function on each render.
Go check the manual:
https://reactjs.org/docs/handling-events.html
And I'm aware that in the last two examples it does create functions on render. But react manual also shows this example and says:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
The problem with this syntax is that a different callback is created
each time the LoggingButton renders. In most cases, this is fine.
However, if this callback is passed as a prop to lower components,
those components might do an extra re-rendering. We generally
recommend binding in the constructor or using the class fields syntax,
to avoid this sort of performance problem.
BETTER SOLUTION
So, if you only need to pass one value, then check out the other examples in the manual. Basically you can bind the method in the constructor (or use an experimental syntax to avoid that step).
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
And how would you get the id/value that you are trying to get? See this example:
class App extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick({currentTarget}) {
console.log(currentTarget.value) // e.currentTarget.value would be equivalent
}
render() {
return (
<button value="here!" onClick={this.handleClick}>
Click me
</button>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
body {
padding: 5px;
background-color: white;
}
<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="root"></div>
So, if you are using buttons or any form element (accepting a value), you may definitively consider this syntax.
You can do this as follows :
class Test extends React.Component {
constructor(props){
super(props);
this.state = {
items: [
{item: "item", id: 1},
{item1: "item1", id: 2}
]
}
}
handleClick(id, e){
alert(id);
}
render(){
return(
<ul id="todo">
{this.state.items.map((item,i) =>
<li className='list-group-item' key={i} data-id={item.id}>{item.name}
<button onClick={this.handleClick.bind(this, item.id)}>X</button>
</li>
)}
</ul>
)
}
}
React.render(<Test />, document.getElementById('container'));
Here is jsfiddle.
Mayid is correct that it is not good to declare or bind functions in render method if the function is passed into other component as a props.
Unfortunately currentTarget didn't work for me.
I have used getAttribute function of event.target. This doesn't cause unnecessary rerenders.
class App extends React.Component {
handleClick = (event) => {
console.log(event.target.getAttribute('index'))
}
render() {
return (
<button index="1" onClick={this.handleClick}>
Click me
</button>
);
}
}
Below is a running sample;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [{
id: 0,
name: "Buy milk"
}, {
id: 1,
name: "Write unit tests"
}, {
id: 2,
name: "Cook a meal"
}]
}
this.handleClick = this.handleClick.bind(this);
}
handleClick(value) {
console.log(`${value} clicked`);
}
renderTodos() {
return this.state.items.map((item, idx) => {
return ( < li className = 'list-group-item'
key = {
idx
} > {
item.name
} < button onClick = {
() => this.handleClick(item.id)
} > X < /button>
</li >
)
})
}
render() {
return ( < ul id = "todo" > {
this.renderTodos()
} < /ul>
)
}
}
ReactDOM.render(
<App/ > ,
document.getElementById('react_example')
);
<!DOCTYPE html>
<html>
<head>
<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>
<meta charset="utf-8">
</head>
<body>
<div id="react_example"></div>
</body>
</html>

Categories