I'm just getting started with React. I have a project that includes many tables, some pretty complex, but all generally output a table from a set of data. Some have parent-child relationships (with toggleable child rows), and each has a slightly different format for the row output (e.g. some have buttons that open modals). Normally, I would use jQuery DataTables for this, which is easy and flexible for something like this. I'm struggling to figure out how to do it sustainably (scaleably?) in React, though.
I've written a basic table component where it accepts a set of items via props and spits out a table and handles child rows via internal state. Today I'll convert that to use two components: the table and a separate one for the rows. I really don't want to have to write X or 2X different components though (where X is the number of tables in the project), but I'm not sure how to make it reusable. All of the tables should have some things in common like style, filter capability, paging, etc., which I would try to put at the Table component level and reuse.
The best solution so far that I've thought about doing is passing in a preRender function via props and using that to create the actual row JSX, and having the render function just assemble all of those snippets into one output (which is basically what I do already but with Array.map. I could then provide the preRender via a prop (if that works), like this:
var Table = React.createClass({
render: function() { // mixin?
var rows = [];
for (var i=0; i<this.props.items.length; i++) {
rows.push(this.props.preRender(this.props.items[i]));
}
return rows; // plus html boilerplate...
}
});
var Parent = React.createClass({
render: function() {
var foodItems = this.state.foodItems;
var drinkItems = this.state.drinkItems;
var foodRender = function(i) { return (<tr>{i} <a href?>Buy me</a></tr>); }
var drinkRender = function(i) { return (<tr>{i} <button>Drink me</button></tr>); }
return (
<Table items={foodItems} preRender={foodRender}/>
<Table items={drinkItems} preRender={drinkRender}/>
);
}
});
Another thing I thought of was somehow passing in a different Row component to the Table component, if that's possible. I guess the root problems are:
The table-level stuff is very similar or identical across tables but may have parts needing customization per-table.
The rows do things like open popovers, so they will have a state (or need to circle around w/props).
Is there a better way to do this sort of logic/dependency injection, or will I probably just need to make a ton of slightly-different controls.
I'd start with something like this (using ES6 syntax for brevity):
const FoodItem = React.createClass({
render() {
return (
<tr><td>{this.props.item} Buy me</td></tr>
);
}
});
const DrinkItem = React.createClass({
render() {
return (
<tr><td>{this.props.item} <button>Drink me</button></td></tr>
);
}
});
const Table = React.createClass({
render() {
const {items, itemComponent: ItemComponent} = this.props;
return (
<table>
{items.map(item => <ItemComponent item={item} />)}
</table>
);
}
});
const Parent = React.createClass({
render() {
return (
<div>
<Table items={this.state.foodItems} itemComponent={FoodItem}/>
<Table items={this.state.drinkItems} itemComponent={DrinkItem}/>
</div>
);
}
});
An alternative pattern would be this (which is best depends on your requirements):
const FoodItem = React.createClass({
render() {
return (
<tr><td>{this.props.item} Buy me</td></tr>
);
}
});
const DrinkItem = React.createClass({
render() {
return (
<tr><td>{this.props.item} <button>Drink me</button></td></tr>
);
}
});
const Table = React.createClass({
render() {
// I'm assuming you ultimately want your table to do something cleverer than just rendering a table element
return (
<table>{this.props.children}</table>
);
}
});
const Parent = React.createClass({
render() {
return (
<div>
<Table>{this.state.foodItems.map(item => <FoodItem item={item} />)}</Table>
<Table>{this.state.drinkItems.map(item => <DrinkItem item={item} />)}</Table>
</div>
);
}
});
Related
I'm working on a dynamic form where we can have a variable amount of facilities. I want to be able to remove specific facilities as needed, but I'm having trouble with making the delete work properly. If I have three facilities and delete the second one, the page always appears like I deleted the last one, even though the state correctly reflects the deletion.
I've tried looking into how rendering and updating works, but I'm rather at a loss at how React does this entire process. It correctly renders that a deletion occurs, but it doesn't update the props of the child components correctly (I believe).
class Request extends React.Component {
constructor(props) {
super(props);
this.state = {
...
facilities: [],
...
}
this.removeFacility = this.removeFacility.bind(this);
}
removeFacility = (number) => (event) => {
var newArray = [...this.state.facilities];
var index = -1;
for (var i = 0; i < newArray.length; i++) {
if (newArray[i].number === number) {
index = i;
}
}
if (index !== -1) {
newArray.splice(index, 1);
} else {
return;
}
for (var i = 0; i < newArray.length; i++) {
newArray[i].number = i + 1;
}
this.setState({facilities: newArray});
}
render() {
const { classes } = this.props;
return (
...
<div>
{this.state.facilities.map(function(f, idx) {
return (
<FacilityRequest
key={idx}
facility={f}
removeFacility={this.removeFacility.bind(this)}/>
)
}, this)}
</div>
...
);
}
}
class FacilityRequest extends React.Component {
...
render() {
const { classes, key, facility, removeFacility} = this.props;
return (
<Paper>
...
{facility.number}
<Button onClick={removeFacility(facility.number)}>Delete</Button>
...
Various form fields
...
</Paper>
);
}
}
When I click delete for a specific facility, the state correctly deletes the facility I want gone, but the last facility is the one that is removed (the other fields in the form remain the same as the one that was supposed to be removed, not matching the state.)
This is probably because you're using the array index as a key to the components you're rendering.
The short explanation is that React uses keys to determine when to re-render mapped components. By using index as a key, and you remove the item at index 4, React will not see that index 4 was deleted, but that it was modified, since something else took its place. This can have some strange side-effects, such as what you're seeing.
You're better off providing a unique identifier to key, such that it identifies the actual element and its content. In this case, facility.number could be a good candidate:
{this.state.facilities.map(function(f, idx) {
return (
<FacilityRequest
key={f.number}
facility={f}
removeFacility={this.removeFacility.bind(this)}/>
)
}, this)}
The best key to use will depend on your implementation, of course.
Using an index as a component key should generally be a last resort.
I am trying to make a application in react for rendering a tree like structure which can be expanded and collapsed on user input, although i have managed to get the app working as I want but the performance is quite slow. I am not sure if this is because of the nature of the application, the react component or my ignorance of the framework.
I have done a chrome profiling and here are the screenshots:
Please if you can help me understand through this images what is the bottleneck and if/how it can be solved.
Source :
https://github.com/harsh-a1/react-skeleton/tree/tree
Component :
export function TreeComponent(props){
var instance = Object.create(React.Component.prototype)
var state = {
previousSelected :{},
onSelectCallback : props.onSelectCallback
}
instance.props = props;
var toggle = function(){
instance.setState(state.data)
}
instance.updateState = function(){
instance.setState(Object.assign({},state))
}
if (!props.data){
init(function(ous){
state.data = ous;
instance.setState(state)
});
}
instance.render = function(){
if (!state.data){return <div key = "dummy"></div>}
return <ul key={"ul_"+state.data.id}>
<Tree data={state.data} updateState={instance.updateState} state={state } />
</ul>
}
return instance;
function Tree(props){
var instance = Object.create(React.PureComponent.prototype)
instance.render = function(){
if (!props.data.children || props.data.children.length == 0){
return (
<li key={"li_"+props.data.id}>
<LeafNode data={props.data} updateState = {props.updateState} state={props.state} />
</li>
)
}
return (
<li key={"li_"+props.data.id}><LeafNode data={props.data} updateState = {props.updateState} state={props.state} />
<ul key = {"ul_"+props.data.id} style={props.data.showChildren?{"display":"inline"}:{"display":"none"}}>
{
props.data.children.map(function(child){
return <Tree data={child} key={"tree_"+child.id} updateState = {props.updateState} state={props.state} />
})
}
</ul></li>
)
}
return instance;
function LeafNode(props){
var instance = Object.create(React.PureComponent.prototype)
instance.props = props;
/* instance.shouldComponentUpdate = function(nextProps) {
return (nextProps.data.showChildren !== this.props.data.showChildren);
}
*/
instance.componentDidMount= function(){
console.log("yes")
}
instance.toggle = function(){
props.data.showChildren = !props.data.showChildren;
props.updateState();
}
instance.selected = function(){
props.state.previousSelected.selected = false;
props.data.selected = !props.data.selected;
props.state.previousSelected = props.data;
props.updateState();
props.state.onSelectCallback(Object.assign({},props.data));
}
instance.render = function(){
var toggleImg = "";
if ( props.data.children.length!=0){
toggleImg = props.data.showChildren ?expandIMG:collapseIMG;
}
return (
<div key={"div_"+props.data.id} >
<span key={"span_"+props.data.id} className="toggle" >
<img key={"img_"+props.data.id} width="12" height="12" src={toggleImg} onClick={instance.toggle} />
</span>
<a key={"a_"+props.data.id} onClick = {instance.selected} style={props.data.selected? {color:"yellow"}:{color:"black"}} >{props.data.name}</a>
</div>
)
}
return instance
}
}
}
Thanks
harsh
Have a look at best practices how to create components and component lifecycle at React website. It is a good idea to follow them so it would be easier to identify problems later.
It is also worth looking at react-virtualized components. There are a bunch of components that could be reused including list, grid, tree etc. Also look at their implementation since it is opensource.
Their virtual list component resolved my issue with rendering 500+ items.
Here is an example with 1M+ nodes and good performance. The trick is to use local state and not render the hidden elements.
https://codesandbox.io/s/z6jr6zww4l
Turns out The issue was that I was using the "development" build to check it....i switched to a production library and now it is running not to bad...still not as good as direct DOM but pretty close...although don't know how much it can scale...
I don't think I fully understand how the Parent/Child relationship works in React. I have two components, Column and Space. When a Column is rendered, it creates some Spaces. I thought that meant that the Column would be the parent to those Spaces, but either I'm wrong or I'm using some part of React.Children.count(this.props.children) incorrectly - it always tells me that any given Column has 0 children.
Simplified models:
var Column = React.createClass({
getInitialState: function() {
return {childCount: '', magicNumber: this.props.magicNumber};
},
componentDidMount: function() {
var newCount = React.Children.count(this.props.children);
this.setState({childCount: newCount});
},
render: function() {
return (
<div className='Column'>
{
Array.from({ length: this.state.magicNumber }, (x,y) => <Space key={y}/>)
}
</div>
);
}
});
var Space = React.createClass({
render: function() {
return (
<div className="Space">
space here
</div>
);
}
});
It seems like no matter where I put React.children.count(this.props.children) in a Column, it tells me there are 0 children. I would expect the Column generated in the example code to have five children, since five Spaces are created within it.
I figured maybe I was trying to count the Children before they were fully loaded, so I tried writing a click handler like this:
//in Column
setChildCount: function () {
var newCount = React.Children.count(this.props.children);
this.setState({childCount: newCount});
}
Then adding a button like this in my Column:
...
render: function() {
return (
<div className='Column'>
{
Array.from({ length: this.state.magicNumber }, (x,y) => <Space key={y}/>)
}
<button onClick={this.setChildCount} />
{this.state.childCount}
</div>
);
But my childCount is always and forever 0. Can anyone see where I'm going wrong?
Edit: Ultimately, I want to get a count of all children elements who have X attribute in their state set to Y value, but I am clearly a step or three away from that.
The Column component doesn't have any children on that code. Childrens are components which are wrapped by the parent component. So imagine:
<Column>
<Space/>
<Space/>
<Column/>
In this case the parent Column has two children Space
On your code:
<div className='Column'>
{
Array.from({ length: this.state.magicNumber }, (x,y) => <Space key={y}/>)
}
</div>
You are creating new components inside a divnot inside the component Column
You are rendering Space components as part of the Column component. The parent/child relationship captured by this.props.children looks like this:
var Column = React.createClass({
render: function() {
<div className="column">
{this.props.children}
</div>
}
});
ReactDOM.render(
<Column>
<Space /> //little space children
<Space />
</Column>
);
To get at your specific problem, you don't need anything like this.props.children because you have everything right there in your render method.
So the answer to your question is: apply the same logic as when you render them.
Example: https://jsfiddle.net/wbellman/ghuw2ers/6/
In an application I am working on, I have a parent container (List, in my example) that contains a list of children (Hero, in my example). The list is governed by an outside object. For simplicity I declared the object directly in the JS. (In my real application the data store is properly namespaced and so forth.)
The problem I have is in the list I have three elements, if I remove an item from the middle, the rendered list appears to remove the last element. However the outside object reflects the proper list.
For example:
My list has the elements: cap, thor, hulk
If you remove "thor", "cap" and "thor" are rendered
The heroList reflects "cap" and "hulk" as it should
I am relatively new to ReactJs, so there is a good chance my premise is fundamentally flawed.
Note: The example reflects a much more complex application. It's structured similarly for purposes of demonstration. I am aware you could make a single component, but it would not be practical in the actual app.
Any help would be appreciated.
Here is the code from JSFiddle:
var heroList = [
{ name: "cap" },
{ name: "thor"},
{ name: "hulk"}
];
var List = React.createClass({
getInitialState() {
console.log("heros", heroList);
return {
heros: heroList
};
},
onChange(e){
this.setState({heros: heroList});
},
removeHero(i,heros){
var hero = heros[i];
console.log("removing hero...", hero);
heroList = _.filter(heroList, function(h){ return h.name !== hero.name;});
this.setState({heros:heroList});
},
render() {
var heros = this.state.heros;
var createHero = (hero,index) => {
return <Hero hero={hero} key={index} onRemove={this.removeHero.bind(this,index,heros)}/>;
};
console.log("list", heros);
return (
<ul>
{heros.map(createHero)}
</ul>
)
}
});
var Hero = React.createClass({
getInitialState() {
return {
hero: this.props.hero
}
},
render() {
var hero = this.state.hero;
return (
<li>Hello {hero.name} | <button type="button" onClick={this.props.onRemove}>-</button></li>
);
}
});
ReactDOM.render(
<List />,
document.getElementById('container')
);
Additional: I was having problems copying the code from JSFiddle, anything I broke by accident should work in the JSFiddle listed at the top of this question.
Edit:
Based on the commentary from madox2, nicole, nuway and Damien Leroux, here's what I ended up doing:
https://jsfiddle.net/wbellman/ghuw2ers/10/
I wish there was a way to give everyone credit, you were all a big help.
Changing your Hero class to this fixed the issue of displaying the wrong hero name for me:
var Hero = React.createClass({
render() {
return (
<li>Hello {this.props.hero.name} | <button type="button" onClick={this.props.onRemove}>-</button></li>
);
}
});
i.e. I removed the local state from the class and used the prop directly.
Generally speaking, try to use the local store only when you really need it. Try to think of your components as stateless, i.e. they get something through the props and display it, that's it.
Along these lines, you should consider passing the hero list through the props to your List component as well.
if you really have problems with managing your data you should use Flux or Redux.
in this code:
heroList = _.filter(heroList, function(h){ return h.name !== hero.name;});
i just dont get why you filer the heroList instead of this.state.heros? every time you add or remove a hero, the heroList in your current scope shouldnt be kept in state? the global heroList is just the initial state.
The problem is with the keys used. Since the key is taken from the index, that key has already been used and thus the hero with that key is shown.
change it to key={Math.random() * 100} and it will work
I am attempting to write a <Heading /> component that will abstract away some of the headaches of dealing with headings (h1, h2, etc) in conjunction with accessibility standards.
The goal being that this component would be able to dynamically choose a h(1,2,3) depending on its closest parent heading. However, Looking up the DOM tree like this remind me more of jQUERY than react. I've looked through the docs and SO but haven't seen anything about it, so I'm not sure if this is even possible.
So the question is: Is it possible for a component to know where it is rendered in the DOM tree and then execute some logic with that info? (probably somewhere in componentWillMount).
You could look upward in the DOM in componentDidMount and componentDidUpdate, but that's messy and uncontrolable. There may be a better solution but this is the first thing that comes to mind.
var headerFactories = ['h1', 'h2', 'h3', 'h4'].map(function(tag){
return React.createFactory(tag)
});
var makeHeader = function(level){
var make = function(){
var factory = headerFactories[make.level]
|| headerFactories[headerFactories.length - 1];
return factory.apply(this, arguments);
};
make.sub = function(){
return makeHeader(make.level + 1);
};
make.level = level;
return make;
}
The api may seem a bit strange, but let's say we have a Page component and an Article child which may have another Article child.
var Page = React.createClass({
render: function(){
// make a root level header
var header = makeHeader();
return <div>
{header(null, "My Page")}
<Article headerFactory={header.sub()} data={foo} subArticle={bar} />
</div>
}
});
var Article = React.createClass({
render: function(){
var subArticle = false;
if (this.props.subArticle) {
subArticle = <Article headerFactory={this.props.headerFactory.sub()} />
}
return <div>
{this.props.headerFactory(null, this.props.data.title)}
</div>
}
});
What happens is when we do headerFactory(null, "foo") we get a component of that header level. When we call .sub() we get a version of that headerFactory with the next header level.
var h1 = makeHeader();
var h2 = h1.sub();
var h3 = h2.sub();
var h4 = h3.sub();
This allows components to have header levels based on their parents without breaking out of React.