I am trying to remove a div when onClick is pressed. The div exists on my parent component where I have
render() {
const listPlayers = players.map(player => (
<Counter
key={player.id}
player={player}
name={player.name}
sortableGroupDecorator={this.sortableGroupDecorator}
decrementCountTotal={this.decrementCountTotal}
incrementCountTotal={this.incrementCountTotal}
removePlayer={this.removePlayer}
handleClick={player}
/>
));
return (
<ContainLeft style={{ alignItems: 'center' }}>
<ProjectTitle>Score Keeper</ProjectTitle>
<Copy>
A sortable list of players that with adjustable scores. Warning, don't go negative!
</Copy>
<div>
<Stats totalScore={this.state.totalScore} players={players} />
{listPlayers}
</div>
</ContainLeft>
);
}
It passes props to the child component where the button to delete the div, here
return (
<div
style={{ display: this.state.displayInfo }}
className="group-list"
ref={sortableGroupDecorator}
id="cell"
>
<CountCell style={{ background: this.state.color }}>
<Row style={{ alignItems: 'center', marginLeft: '-42px' }}>
<Col>
<DeleteButton onClick={removePlayer}>
<Icon name="delete" className="delete-adjust fa-minus-circle" />
</DeleteButton>
</Col>
(I snipped the rest of the code because it was long and not useful here)
The array (a separate file) is imported into the Parent component and it reads like this
const players = [
{
name: 'Jabba',
score: 10,
id: 11
},
{
name: 'Han',
score: 10,
id: 1
},
{
name: 'Rey',
score: 30,
id: 10
}
];
export default players;
So what I'm trying to do is write a function on the main parent that when it is clicked inside the child, the div is removed, deleted, gone (whatever the best term is) sort of like "remove player, add player"
On my parent component, I've written a function where the console.log works when it is clicked in the child, but whatever I write in the function doesn't seem to want to work.
The function I'm building (in progress, I'm still a little lost here) is:
removePlayer() {
console.log('this was removed');
players.splice(2, 0, 'Luke', 'Vader');
}
which is mapped over here as a prop
const listPlayers = players.map(player => (
<Counter
key={player.id}
player={player}
name={player.name}
sortableGroupDecorator={this.sortableGroupDecorator}
decrementCountTotal={this.decrementCountTotal}
incrementCountTotal={this.incrementCountTotal}
removePlayer={this.removePlayer}
handleClick={player}
/>
));
And passed into the child here:
render() {
const {
name,
sortableGroupDecorator,
decrementCountTotal,
incrementCountTotal,
removePlayer
} = this.props;
return (
<div
style={{ display: this.state.displayInfo }}
className="group-list"
ref={sortableGroupDecorator}
id="cell"
>
<CountCell style={{ background: this.state.color }}>
<Row style={{ alignItems: 'center', marginLeft: '-42px' }}>
<Col>
<DeleteButton onClick={removePlayer}>
<Icon name="delete" className="delete-adjust fa-minus-circle" />
</DeleteButton>
I know all this is lengthy and I wanted to provide as much detail as I could because React is still new to me and I get confused with some of the verbiages. Thanks for helping out in advance
We sorted it out in chat. Like expected, it was a problem with the state.
I made a small semi-pseudo snippet with comments as explanation:
import React, { Component } from 'react';
// Your player constant, outside the scope of any React component
// This pretty much just lives in your browser as a plain object.
const players = [
{
name: 'Jabba',
score: 10,
id: 11
},
{
name: 'Han',
score: 10,
id: 1
},
{
name: 'Rey',
score: 30,
id: 10
}
];
class App extends Component {
constructor() {
super();
this.state = {
players, // ES6 Syntax, same as players: players
// Add all your other stuff here
};
}
removePlayer(id) {
const newState = this.state;
const index = newState.players.findIndex(a => a.id === id);
if (index === -1) return;
newState.players.splice(index, 1);
this.setState(newState); // This will update the state and trigger a rerender of the components
}
render() {
const listPlayers = this.state.players.map(player => { // Note the this.state, this is important for React to see changes in the data and thus rerender the Component
<Counter
..
removePlayer={this.removePlayer.bind(this)} //bind this to stay in the context of the parent component
/>
});
return (
<div>
{listPlayers}
</div>
);
}
}
//////////////////////////////////////// Child component
....
<DeleteButton onClick={() => this.props.removePlayer(this.props.player.id)}>
....
Here is how i solved this
i've added an id to the element
and then with function remove i detroyed it
const closenotif = () => document.getElementById("notif").remove()
<div id="notif">
<button onClick={destroy}> close </button>
</div>
NB : the element is destroyed in the current document
so in the current render
on Next JS this works perfectly
if you are using a live rendering with react this probably won't work
i'll suggest you to work with states in that case
Little confused about how the whole app works, but I will try to help you.
To make react changes to the dom, you have to put players in the state. So, in the removePlayer you make a copy of this.state.players in a local variable (just to not change the array directly in state, it's a good practice), then you make the split in this local variable and finally you setState({ players: localPlayers}).
This way the "div" will be removed.
Related
I'm building an React component/EntityBrowser that holds a Tree component from ant.design. It's a prototype going into production soon. It's still based on create-react-app. State is managed through Redux.
I've tried searching for answers using both google and Stackoverflow. The consensus seems to be; whenever the Tree components treeData prop receives too much data, it slows down the rendering process, because each of the TreeNodes needs to be re-rendered every time the Tree is expanded. Since more data means slower rendering for each of the TreeNodes - well.. adding more TreeNodes also means slower rendering by the factor the extra data adds.
I don't know if this is the real reason for the overall performance. I've profiled the component and indeed adding more data results in worse rendering times and same goes for the amount of nodes. I would expect this. I'm just surprised by the rate the Tree performance, both when adding new nodes dynamically, but also when expanding it, deteriorates. It does not require many nodes. After only a few nodes with minimal data, it's already too slow to be usable.
This is the Tree component:
<Tree
rootClassName={dark ? "dark-tree" : ""}
style={{ borderRadius: "0" }}
blockNode={true}
onExpand={onExpand}
onSelect={onSelect}
selectedKeys={selectedKeys}
expandedKeys={expandedKeys}
treeData={treeData}
titleRender={TreeNode}
/>
These are the props handling onExpand and onSelect.
const onExpand = (expandedKeys) => {
dispatch(entityBrowserActions.updateExpandedKeys(expandedKeys));
};
const onSelect = (selectedKeys, { event }) => {
if (handleSelection) handleSelection(selectedKeys);
dispatch(entityBrowserActions.updateSelectedKeys(selectedKeys));
};
The handleSelection call tells the app to show the right "scenario" based on the current entityStructure. We are choosing between them in the EntityBrowser/Tree itself.
The treeData is generated with:
const createTreeData = (entityStructure) => {
return entityStructure.map((item) => {
const itemSelectable = !selectedKeys.includes(item.key);
let title = (<span>{item.name}</span>);
let itemObj = {};
let newName = (e) => onNameChange(e, item.key);
let stopEdit = () => handleStopEdit(item.key);
if (editable && editingEntities[item.key] == true) {
title = (
<Input
value={item.name}
onChange={ newName }
onBlur={ stopEdit }
/>
);
}
if (item.children) {
itemObj = {
title,
key: item.key,
selectable: itemSelectable, // Selectable only if not already selected
checkable: false,
children: createTreeData(item.children),
};
} else {
itemObj = {
title,
key: item.key,
selectable: itemSelectable,
}
}
return itemObj;
});
};
The "design" of the treenodes is added with the titleRender prop:
const TreeNode = (value, record) => {
const handleEdit = () => handleStartEdit(value.key);
const handleClose = () => handleClosePopup(value.key, deletePopoverVisible[value.key]);
const handleDeleteAndClose = () => {
handleDelete(value.key);
handleClosePopup(value.key, false);
}
return (
<div className="tree-title">
{value.title}
{editable ? (
<div style={{}}>
<Button
type="text"
style={{ padding: "2px 4px" }}
onClick={ handleEdit }>
<EditOutlined style={{ fontSize: "1.1rem" }} />
</Button>
<Popover
visible={ deletePopoverVisible[value.key] }
onVisibleChange={ handleClose }
content={
<div style={{ display: "flex", flexDirection: "column" }}>
<Button
type="text"
style={{ color: "red" }}
onClick={ handleDeleteAndClose }>
Delete
</Button>
<Button
type="text"
onClick={ handleClose }>
Cancel
</Button>
</div>
}
trigger="click">
<Button type="text" style={{ padding: "2px 4px" }}>
<DeleteOutlined style={{ fontSize: "1.1rem" }} />
</Button>
</Popover>
</div>
) : null}
</div>
);
};
I've tried to remove all the "unnecessary" props like onSelect, titleRender, blockNode, etc. When doing this it does give a speed up but even if I remove everything but the bare minimum it is still painfully slow. Initially all the function calls happened on props inline and now I've "pulled them out" to try and gain some speed. Got a little bit - but not nearly enough.
I don't know how to proceed here. I'm hoping someone can point me to something super obvious that I'm missing.
Everything and nothing worked.
I have built a minimal ReactJS component to update the number of likes. All works well but the count does not update when clicked. I tried following several answers but cannot figure out why.
See the code:
import React, {useState} from 'react';
class GiveLikes extends React.Component {
// set the initial value of the likes
state = {likes:0};
// Function is called every time "likes" is clicked
likes_count = (likes) =>{
// Counter state is incremented
this.state({likes: likes+1});
}
render() {
return (
<>
<h2> {this.state.likes} </h2>
<div className="buttons">
<button style={{
fontSize: '60%',
position: 'relative',
top: '20vh',
marginRight: '5px',
backgroundColor: 'green',
borderRadius: '8%',
color: 'white',
}}
onClick={() => this.likes_count}>Likes
</button>
</div>
</>
)
}
}
export default GiveLikes;
The above code will render the following on the web browser. Clicking the "Likes" should update the value of the count, but unfortunately it does not.
Declare a constructor and initialize your state,
Use an arrow function on your likes_count() method
Use this.setState({likes: this.state.likes +1}); instead of this.state({likes: this.state.likes +1});
import React, {useState} from 'react';
class GiveLikes extends React.Component {
constructor(props) {
super(props);
this.state = {likes: 0};
}
likes_count = () => {
this.setState({likes: this.state.likes +1});
}
render() {
return (
<>
<h2> {this.state.likes} </h2>
<div className="buttons">
<button style={{
fontSize: '60%',
position: 'relative',
top: '20vh',
marginRight: '5px',
backgroundColor: 'green',
borderRadius: '8%',
color: 'white',
}}
onClick={this.likes_count}>Likes
</button>
</div>
</>
)
}
}
export default GiveLikes;
Edit:
The summary of this answer is that the state does not exist because there is no constructor for this.state
I believe the answer to be is Props are never to be updated. We are to use them as is. Sounds rigid right? But React has its reasons behind this rule and I’m pretty convinced by their reasoning. The only caveat is, though, that there are situations where we might need to initiate the update of a prop. And we will soon know how.
Consider the following line of code from a parent component:
<MyChild childName={this.state.parentName} />
Now if there is any change of name required, parentName will be changed in the parent and that change will automatically be communicated to the child as is the case with the React mechanism. This setup works in most of the scenarios.
But what if you need to update the prop of the child component, and the knowledge of the change required and the trigger to change it is only known to the child? Considering the ways of React, data can only flow from top-to-bottom i.e., from parent-to-child. So then how are we to communicate to the parent that a change to prop is required?
Answer from the following source: https://www.freecodecamp.org/news/how-to-update-a-components-prop-in-react-js-oh-yes-it-s-possible-f9d26f1c4c6d/
I've only shown the parts you need to read nothing else. Please analyse your code properly next time, this isn't a hard thing to do .
You forgot to call the function -
onClick={() => this.likes_count()}
Also, Instead of passing likes you need to use the data from state and then update it like -
likes_count = () => {
let likes = this.state.likes;
this.setState({ likes: likes + 1 });
};
Add a constructor and initialize this.state otherwise it won't be exists.
What happens is every time you re-render your component (by state or props change) you will re-create the your state again and again with {likes: 0} and it will not work.
Also, you are mixing class component and functional component syntax style, which will lead to more bugs and issues with react and your code.
Moreover, you need to put a function that will be called in onClick but you created a function that returns a function, and it is wrong in your case.
Another issue is to set your likes state using this.setState and not just calling state as a function.
import React from 'react';
class GiveLikes extends React.Component {
constructor(props) {
super(props);
this.state = {likes: 0};
}
likes_count = () => {
this.setState ({likes: this.state.likes +1});
}
render() {
return (
<>
<h2> {this.state.likes} </h2>
<div className="buttons">
<button style={{
fontSize: '60%',
position: 'relative',
top: '20vh',
marginRight: '5px',
backgroundColor: 'green',
borderRadius: '8%',
color: 'white',
}}
onClick={this.likes_count}>Likes
</button>
</div>
</>
)
}
}
export default GiveLikes;
Read about react.
Focus on your first component (look for examples with counters components online).
Don't rush into things without fully understand. React is fun.
I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log() statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log() I would actually call some other function.
I find that the console.log() statement executed on initial page render itself, even though in=false initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter and unmountOnExit, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Problem
The children are rendered on initial load because they're defined within the Row component.
Solution
Move the Collapse children to its own React component. This won't render the children until the Collapse is opened. However, it'll re-render the child component when Collapse is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open to this component and use it as an useEffect dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
Demo
Code
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example component being used as children to Collapse (also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
Other Thoughts
If the API data is static and/or doesn't change too often, I'd recommend using data as a dependency to useEffect (similar to the open example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log() statement to the render() of the new child component (<Example>), I still see it being executed before it is mounted.
Adding mountOnEnter and unmountOnExit solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag is essential to avoid the function execution during closing of the <Collapse>.
I want to handle disable attribute of input elements by ID in my React app
It's possible with states but number of elements is not fixed, maybe 10 input or 20 or more ...
I've decided to set ID for each input and access to them with ID, for example :
document.getElementById('title-12') ....
So, is it a suitable trick or best practice to handle this issue ?
Performance and clean code is very important for me :-)
Thanks
Oops... my bad. I digged into your discussion and here's a new solution.
That's still correct that React approach is advised so we should use reusable components for inputs (we can have any number of inputs right now). All the input's data are stored in parent component's store as a collection. We map through collection and send properties to each component (in the simplest version - id, isDisabled and disableInput() function).
class Titles extends Component {
constructor(props) {
super(props);
this.state = {
titles: [
{
id: 0,
disabled: true
},
{
id: 1,
disabled: false
}
]
};
}
addNewInput = () => {
const prevList = this.state.titles;
const newItem = {
id: prevList.length,
disabled: false
};
this.setState({ titles: [...prevList, newItem] });
};
disableInput = id => {
const titles = this.state.titles;
titles[id].disabled = !titles[id].disabled;
this.setState({ titles });
};
render() {
return (
<div>
<h1>Titles list</h1>
<form style={{ display: "flex", flexDirection: "column" }}>
{this.state.titles.map(title => (
<Title
key={title.id}
id={title.id}
isDisabled={title.disabled}
disableInput={id => this.disableInput(id)}
/>
))}
</form>
<button onClick={() => this.addNewInput()}>Dodaj nowy</button>
</div>
);
}
}
in Title component we just render the props in to <input> and a button with onClick function that sends id of this component to its parent, where disable attribute's value is being reversed.
const Title = ({ id, isDisabled, disableInput }) => {
return (
<div>
<input
id={id}
type="text"
placeholder={id}
disabled={isDisabled}
/>
<button type="button" onClick={() => disableInput(id)}>
disable input
</button>
</div>
);
};
Working example can be found here.
Please let me know if it works for you.
am refactorizing huge project in which
some components look very similar
there s looots of files (very high level of components vs files granulation)
I've thought and searched for ways to handle this issue here and there and found this great article about Higher Order Components (HOC) - basically being components that wrap another components.
https://medium.com/#franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.8vr464t20
I will now give you (A) example of two out of eight similar components types that I need to handle, than (B) I will paste the code I came up with that unifies those eight files into one. Finally (C) will paste example of usage of that unified component.
I will try to be consistent and naming will be not domain driven (I can't post project details here) but same names in different code excerpts below will always point to same components and data. Otherwise I will point it.
(A) - similar components types
1. TabA - Simple one
export default class TabA extends Component {
render() {
return (
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something }/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history={ this.props.history }
activeTab={ Paths.somePath }
/>
<TabAContent
data={ this.props.data }
name={ this.props.name }
someValue={ this.props.someValue }
/>
</GridItem>
</Grid>
</PageWrapper>
);
}
}
Notice that SomeComponentA does not take any children in. Also there is no conditional rendering of any kind here.
2. TabB - More complex one
Similarly here, notice that renderSomeData method conditionally renders SmartComponentToBeConditionallyRendered and also SomeComponentB takes children in from the props.
export default class TabB extends Component {
renderSomeData() {
let someData = {
header: "Header text",
searchPlaceHolder: 'Search (name)',
buttonCaption: 'button caption'
};
return (
<SmartComponentToBeConditionallyRendered
type={ 'some_type' }
permalink={ this.props.permalink }
data={ someData }
/>
)
}
render() {
let { data } = this.props;
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something}/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { Paths.somePage }
/>
<TabBContent data = { data }>
{this.props.children}
</TabBContent>
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission('somePermission') ?
this.renderSomeData() :
null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
}
Those eight components I've wrote about at the beginning - they all represent one of three possibilities.
Two pictured above and possibility C, but differences in C are just another conditionally rendered components so not worth mentioning cause it will finally come down to passing more flags in props.
Those two components above - they differ in:
kind of SomeComponentX - in place of X there may A, B but also C, D, E etc. in every of those eight similar components. Each of SomeComponentX take in different props as well.
Paths.VALUE_HERE
if SomeComponentX takes in any children or not
if they conditionally render data from renderSomeData method
if YES - someData defined inside the method varies as well
permalink
some_type
(B) What I came up with
let availablePartials = {
PartialA: PartialA,
PartialB: PartialB,
PartialC: PartialC
}
export default class GenericTab extends Component {
renderSomeData() {
return (
<SomeData
type = { this.props.type }
permalink = { this.props.permalink }
data = { this.props.someData } //PASSED FROM PROPS NOW
/>
);
}
render() {
let tabContent = React.createElement(
availablePartials[this.props.partialView.name],
this.props.partialView.props,
this.props.renderChildren ? this.props.children : null
);
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something = { this.props.permalink }/>
</GridItem>
<GridItem xsSize = "9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { this.props.activeTab }
/>
{ tabContent }
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission(this.props.requiredPermission) && this.props.dataForSomeDataMethod ?
this.renderSomeData()
: null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
};
CityPageTab.propTypes = {
permalink: PropTypes.string,
dataForSomeDataMethod: PropTypes.object,
type: PropTypes.string,
activeTab: PropTypes.string,
renderChildren: PropTypes.bool,
partialView: PropTypes.object,
requiredPermission: PropTypes.string
};
Basically EVERYTHING is constructed from props. The only part I don't like is availablePartials[this.props.partialView.name].
It requires developers to keep the state of availablePartials object consistent and tangles it a bit. Not nice solution but still it is best I came up with so far.
(C) New GenericTab usage example
componentThatUseGenericTabRenderMethod() {
let { valueA, valueB, valueC, history } = this.props;
let someData = {
header: 'header text',
searchPlaceHolder: 'Search (name)',
buttonCaption: 'buttonCaption'
}
return (
<GenericTab
partialView = {{
name: 'PartialA',
props: {
A: valueA,
B: valueB,
C: valueC,
history: history,
permalink: this.props.params.permalink
}
}}
permalink = { this.props.params.permalink }
activeTab = { Paths.somePath }
someData = { someData }
type = { 'SOME_TYPE' }
renderChildren = { false }
requiredPermission = { 'some_required_permision' }
/>
);
}
So that is that. Usage got bit more complex, but I got rid of seven files (and getting rid of files is main objective as there is too many of them) and am going to further push it in similar manner - generic one.
Thing with genericity - it is more difficult to use but saves lots of space.
Project utilises Redux so dont be too concerned about passing props down the tree. They always only come from some SmartParentComponent that renders GenericTab
Below is the visualisation of how it looks on the page.
GenericTab is responsible for rendering Tabs and TabContent parts.
Yes I know it is shitty solution, but am not responsible for architecture of it. There are so many things to be refactorized here and what am asking about is just a step in a journey. So please lets focus on the question asked and not other things that are so wrong with this code. I know.:)
Guess I could make an article out of it but I don't really have blog to do it:).
Please tell me what you think, propose upgrades, different ways of handling this problem etc.
When dealing with such architecture (i.e, tabs in your case), you basically don't want to hide the architecture under the hood, because in this case your ending up adding more and more properties with each new case you want to handle.
Instead, you wan't to let react handles the nested structure since it's where react really shines. That let you write something very generic by handling the built in children props. You typically want to write something like :
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
The point here is that you don't have to "predict" all the things that might appear under each TabPanel, simply let the developper put whatever he wants ! BUT me need some logic to handle the "go to tab" sort of things.
React provides some very handy utilities methods to dynamically clone elements, map over elements, and render element whether its type is the one you expect or not (in our case, we expect TabBar or TabPanel type, nothing prevent the developper to put any other components than this two but nothing prevent him neither to put any built in <table> html element inside of <a> tag or something weird like that).
Here is a little implementation with Material Design Lite, it's not perfect but you should get the point :
class Tabs extends React.Component {
constructor(props) {
super(props),
this.state = {
activeTabId: props.defaultActive
}
}
tabClickHandlerFactory(id) {
return (e) => {
e.preventDefault()
this.setState({
activeTabId: id
})
}
}
getPanelIdFromLink(href) {
return href.split('#')[1]
}
render() {
const self = this
return (
<div className='mdl-tabs is-upgraded' {...self.props}>
{React.Children.map(self.props.children, (child, index) => {
if (child.type == TabBar) {
return React.cloneElement(child, {}, React.Children.map(child.props.children, (link) => {
const id = self.getPanelIdFromLink(link.props.href)
return (
React.cloneElement(link, {
onClick: self.tabClickHandlerFactory(id),
active: self.state.activeTabId === id
})
)
}))
}
if (child.type == TabPanel) {
const { id } = child.props
const active = self.state.activeTabId === id
return active && React.cloneElement(child, { active: true })
}
})}
</div>
)
}
}
Tabs.propTypes = {
defaultActive: React.PropTypes.string.isRequired,
}
const TabBar = (props) => <div className='mdl-tabs__tab-bar' {...props}>{props.children}</div>
const TabLink = ({ active, ...props }) => {
return (
<a className={`mdl-tabs__tab${active ? ' is-active' : ''}`} {...props}>{props.children}</a>
)
}
const TabPanel = ({ active, ...props }) => (
<div className={`mdl-tabs__panel${active ? ' is-active' : ''}`} {...props}>{props.children}</div>
)
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
ReactDOM.render(<PageWithTabs/>, 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>
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.brown-orange.min.css" />
<div id='app'></div>