Juggling Animations and View Hierarchy in React - javascript

I'm having some frustrations with React because the view hierarchy is ruining my animations.
I have a <Layout> component that setups a tab-nav layout and its children are placed within the content portion of the layout. It is a pure component that has props for title, setTab, and currentTab.
If I were thinking about this from a view hierarchy perspective and one-directional data-flow, I would have the layout be the child of each view so the view could set the title, current tab, and setTab could be delegated to its parent.
This basically what I am doing:
var A = React.createClass({
propTypes: {
setTab: React.PropTypes.func.isRequired
},
render: function() {
return (
<Layout title="A Page" currentTab="A" setTab={this.props.setTab}>
This is A
</Layout>
)
}
});
Now you can use React Router or something for the tabs, but right now, I just have a simple top-level component switching tabs with its state:
var App = React.createClass({
getInitialState: function() {
return {
currentTab: 'A'
}
},
setTab: function(name) {
this.setState({currentTab:name})
},
render: function() {
var component = false
if (this.state.currentTab == 'A') {
return <A setTab={this.setTab}/>
} else {
return <B setTab={this.setTab}/>
}
}
});
This is all fine and dandy until you want to start adding some animations.
In my <Layout> component, I have a highlight block that I want to animate left to right when switching between tabs.
<div className="tabbar">
<div className={"highlight " + this.props.currentTab}></div>
<div className="tabs">
<div className="tab" onClick={() => {this.props.setTab('A')}}>A</div>
<div className="tab" onClick={() => {this.props.setTab('B')}}>B</div>
</div>
</div>
Here's the CSS for that animation:
.highlight {
border-bottom: 3px solid red;
width: 50%;
position: absolute;
top: 0;
bottom: 0;
transition: left .25s ease-in-out;
}
.highlight.A {
left: 0;
}
.highlight.B {
left: 50%;
}
I'm also trying to animate the Layout's title using a the React.addons.CSSTransitionGroup but I think the issue is the same:
The problem here seems to be that the <Layout> component is entirely re-rendered each time because the root of the render tree (<A/> and <B/>) is changing.
So the question now, is how do I invert the view hierarchy so that <Layout> doesnt keep getting re-rendered so the animation can work?
Note: In a less contrived example, the layout may also get other props from the <A/> such as onClick handlers.
Here's a JSFiddle demonstrating my frustrations:
https://jsfiddle.net/ccorcos/3rupb0og/

Ok. I figured something out that works, but I'm not sure if this is a best practice so I'd really appreciate some feedback before I conclude that this is the answer.
I've come up with a concept of a stitch as a way of stitching functions through the component hierarchy. This will allow me to set the state of a parent component from the child component.
function stitch() {
return {
to: (f) => {
this.func = f
},
from: () => {
this.func.apply(null, arguments)
}
}
}
So now my <Layout> looks like this:
var Layout = React.createClass({
propTypes: {
currentTab: React.PropTypes.string.isRequired,
onTab: React.PropTypes.func.isRequired,
stitchTitle: React.PropTypes.string.isRequired
},
getInitialState: function() {
return {
title: ''
}
},
componentWillMount: function() {
this.props.stitchTitle((title) => {
this.setState({title})
})
},
// ...
}
And the view looks like this:
var A = React.createClass({
propTypes: {
setTitle: React.PropTypes.func.isRequired
},
componentWillMount: function() {
this.props.setTitle('Page A')
},
//...
}
And all we have to do is stitch them together.
var App = React.createClass({
getInitialState: function() {
return {
currentTab: 'A'
}
},
setTab: function(name) {
this.setState({currentTab:name})
},
componentWillMount: function() {
this.titleStitch = stitch()
},
render: function() {
var view = false
if (this.state.currentTab == 'A') {
view = <A setTitle={this.titleStitch.from}/>
} else {
view = <B setTitle={this.titleStitch.from}/>
}
return (
<Layout currentTab={this.state.currentTab} onTab={this.setTab} stitchTitle={this.titleStitch.to}>
{view}
</Layout>
)
}
});
And sure enough it works! Here's the JSFiddle:
https://jsfiddle.net/ccorcos/bLtuLdwh/

Related

ReactJS not able to reference state created

http://jsfiddle.net/adamchenwei/3rt0930z/20/
I just trying to create an example to learn how state works in a list.
What I intent to do is to allow a particular value that got repeated in a list, to change, in ALL items in the list, by using state. For example, in this case, I want to change all the list item's name to 'lalala' when I run changeName of onClick.
However I have this warning (issue at fiddle version 11, resolved at version 15)
Any help on resolving it to achieve purpose above?
Actual Code
var items = [
{ name: 'Believe In Allah', link: 'https://www.quran.com' },
{ name: 'Prayer', link: 'https://www.quran.com' },
{ name: 'Zakat', link: 'https://www.quran.com' },
{ name: 'Fasting', link: 'https://www.quran.com' },
{ name: 'Hajj', link: 'https://www.quran.com' },
];
var ItemModule = React.createClass({
getInitialState: function() {
return { newName: this.props.name }
},
changeName() {
console.log('changed name');
this.setState({ newName: 'lalala' });
},
render() {
//<!-- <a className='button' href={this.props.link}>{this.props.name}</a> -->
return (
<li onClick={this.changeName}>
{this.state.newName}
</li>
);
}
});
var RepeatModule = React.createClass({
getInitialState: function() {
return { items: [] }
},
render: function() {
var listItems = this.props.items.map(function(item) {
return (
<div>
<ItemModule
key={item.name}
name={item.name} />
</div>
);
});
return (
<div className='pure-menu'>
<h3>Islam Pillars</h3>
<ul>
{listItems}
</ul>
</div>
);
}
});
ReactDOM.render(<RepeatModule items={items} />,
document.getElementById('react-content'));
-UPDATE-
fiddle version 16
updated fidle, now there is issue with key, also, the onClick did not update the value for all the list item. Is there something wrong I did?
-UPDATE-
fiddle version 20
Now the only issue is change all the list item's name to 'lalala' when I run changeName of onClick.
remove the parenthesis from
onClick={this.changeName()},
so
onClick={this.changeName}
you want to call the function onClick, but you are calling it on render that way
I think you meant to do onClick={this.changeName}
In the way you have it you are calling the changeName function on render instead of on click.

React renders text slower than style change - Alert component

I'm learning react. In the below Alert component I'm updating a simple alert message and setting the opacity to 1 and then fading it out with a CSS transition. This is in a simple to do app to get ramped up on react.
The issue I'm having is when I update the alert text this.props.message the previous message text shows for a fraction of a second after opacity is set to 1, before the new message replaces it. It is very quick but it seems like there is a race condition as the DOM or React renders the text change slower than the style change. Let me know if there is a way I can omit the hiccup and set opacity after the text message has changed. Thanks.
var TodoApp = React.createClass({
getInitialState: function() {
return {
items: [],
alert: { message: '', success: null }
};
}
genericMethod: function() {
...
this.setState({ alert: { message: message, success: true } });
...
},
...
render: function() {
return (
<div>
<Alert message={this.state.alert.message} success={this.state.alert.success} />
<ItemInput onItemSubmit={this.addItem} />
<ItemList items={this.state.items} deleteItem={this.deleteItem} toggleComplete={this.toggleComplete} />
</div>
);
}
});
...
var Alert = React.createClass({
delayTime: 1000,
getInitialState: function() {
return {
visible: false
};
},
componentWillReceiveProps: function() {
this.setState({ visible: true }, function() {
this.setTimer();
});
},
setTimer: function() {
setTimeout(function(argument) {
this.setState({ visible: false });
}.bind(this), this.delayTime);
},
render: function() {
var style = {
opacity: (this.state.visible ? 1 : 0),
transition: 'opacity '+(this.state.visible ? 0 : this.delayTime)+'ms'
};
var alertClass = 'alert'+(this.props.success ? ' success' : ' error');
return (
<div className={alertClass} style={style}>
{this.props.message}
</div>
);
}
});
The solution was to remove the alert state from the top level Todo class. So { items: [ ], alert: { message: '', success: null } } became just { items: [ ] } and then call a method on the child Alert component from my Todo component with <Alert ref="alert" /> and this.refs['alert'].triggerAlert(message, true); (as seen in this SO answer React.js - access to component methods) instead of watching componentWillReceiveProps in the Alert component. This made it so I was not prematurely rendering the Alert component before the new message was set every time the Todo items were added or updated.

Pass Parent Prop to Children reactjs

I have a Legend, which contains multiple Legend.Items children. I'm having a problem where currently onClick it is possible to deselect all of the Legend Items which has consequences that I'd like to avoid. Is it possible to set some sort of onClick handler in the Legend component that can have some state clicked and check whether there are n - 1 legend items "selected/faded", n being the total number of legend items? I looked at the JSX Spread Attributes, but because I'm using {this.props.children}, I'm not sure how to use them or if they would work in this context.
I also took a look at this blogpost (http://jaketrent.com/post/send-props-to-children-react/), but it looked a bit hacky to me and I thought there might be a more conventional way. I'm new to ReactJS so if I need to provide more context, let me know!
MY CODE:
LEGEND.JSX
var React = require('react');
var cn = require('classnames');
// Have legend hold state about how many clicked
var Legend = React.createClass({
getInitialState: function () {
return { clicked: 0 }
},
render: function () {
console.log(this.props.children);
return (
<ul className="legend inline-list">
{this.props.children}
</ul>
);
},
});
Legend.Item = React.createClass({
getInitialState: function () {
return { hover: false, clicked: false };
},
handleMouseOver: function () {
this.setState({ hover: true });
this.props.mouseOver(this.props.name);
},
handleMouseOut: function () {
this.setState({ hover: false });
this.props.mouseOut(this.props.name);
},
handleClick: function() {
if (this.state.clicked) {
this.setState({ clicked: false });
this.props.click(this.props.name);
} else {
this.setState({ clicked: true });
this.props.click(this.props.name);
};
},
render: function () {
var swatchClasses = cn({ 'swatch': true, 'legend-item-fade': this.state.hover, 'c3-legend-item-hidden': this.state.clicked })
var spanClasses = cn({ 'legend-item-fade': this.state.hover, 'c3-legend-item-hidden': this.state.clicked })
return (
<li className="legend-item">
<i className={swatchClasses}
onClick={this.handleClick}
onMouseEnter={this.handleMouseOver}
onMouseLeave={this.handleMouseOut}
style={{ "backgroundColor": this.props.color }}></i>
<span className={spanClasses}
onClick={this.handleClick}
onMouseEnter={this.handleMouseOver}
onMouseLeave={this.handleMouseOut}>
{this.props.name}
</span>
</li>
);
},
});
module.exports = {
Legend: Legend,
};
RESPONSE.JSX RENDER FUNCTION
<Legend>
{newColumns.map(function (column) {
return (
<Legend.Item name={column.name}
color={column.color}
click={this.onLegendClick}
mouseOut={this.onLegendMouseOut}
mouseOver={this.onLegendMouseOver}/>
);
}.bind(this))}
</Legend>
I think the best and simplest way is to use callbacks.
In Legend recreate the components from the children, augmenting their props with a callback to Legend:
let legendItems = React.Children.map(this.props.children, child =>
React.cloneElement(child, { updateLegendCounter: this.updateLegend})
);
The callback in Legend is something like this:
updateLegend() {
this.setState({clicked: clicked + 1})
}
And finally, in your render method, you discriminate when
if (this.state.clicked === children.length-1)
Also, I would pass the initial state of clicked as a prop to the Item element. In this way it becomes really easy to select/deselect all.

How to communicate between Components with out flux in React.js

Ok, I do know through refs communicate between parent and child or use this.props.onClick = {this.props.onClick}, I got stuck in situation communicate between grandparent and child like this:
Says we have some blogs, once we click a blog title, the corresponding blog content will show, so we create three components: BlogAdmin, BlogTitle and Blog (Here let's just focusing on BlogAdmin and BlogTitle)
When BlogTitle is clicked, I want to notify BlogAdmin set currentblog to specify blog. But I got stuck on how to pass the data and how to trigger the event, better with out using pubSub.
Below is my example, I removed some data get/set and grammars making it clear.
var BlogTitle = React.createClass({
render: function() {
return
<li>{this.props.blog.title}</li>
}
});
var BlogTitles = React.createClass({
render: function() {
return
<ul>
{this.state.blogs.map}
<BlogTitle blog={blog} />
}
})
var BlogAdmin = React.createClass({
render: function() {
return
<BlogTitles />
<BlogContent />
}
})
The easy solution is to add a callback function and send it down all the way like this:
var BlogTitle = React.createClass({
render: function() {
return
<li onClick={this.handleTitleClick}>{this.props.blog.title}</li>
},
handleTitleClick: function() {
this.props.onBlogTitleSelection(this.props.blog);
}
});
var BlogTitles = React.createClass({
render: function() {
return
<ul>
{this.state.blogs.map}
<BlogTitle blog={blog} onBlogTitleSelection={this.props.onBlogTitleSelection} />
}
})
var BlogAdmin = React.createClass({
selectBlogTitle: function(blog) {
// act!
},
render: function() {
return
<BlogTitles onBlogTitleSelection={this.selectBlogTitle} />
<BlogContent />
}
})

Reactjs managing stateful children

I have a dynamic list of children, that are form inputs.
ex:
var FormRows = React.createClass({
getInitialState: function() {
return {
rows: []
}
},
createRows: function() {
this.props.values.maps(value){
rows.push(<FormRow ...handlers... ...props... value={value} />
}
},
addNewRow{
// add a new row
},
render: function() {
return (
<div>
{this.state.rows}
</div>
);
});
var FormRow = React.createClass({
getInitialState: function() {
return {
value: this.props.value || null
}
},
render: function() {
<input type='text' defaultValue={this.state.value} ...changeHandler ... }
}
});
This is a dumbed down version , but the idea, is a its a dynamic form, where the user can click a plus button to add a row, and a minus button, which will set the row to visibility to hidden.
This state is nested n levels deep. What is the best way to actually get the state out of the children, and submit the form? I can use 'ref' add a function to getFormValue(): { return this.state.value } to the FormRow button, but i'm not sure if thats the best practice way.
I find myself using this pattern quite often, an array of undetermined size of children, that need to pass the state up.
Thanks
It’s not a dumb question at all, and a good example of using flux principals in React. Consider something like this:
var App
// The "model"
var Model = {
values: ['foo', 'bar'],
trigger: function() {
App.forceUpdate()
console.log(this.values)
},
update: function(value, index) {
this.values[index] = value
this.trigger()
},
add: function() {
this.values.push('New Row')
this.trigger()
}
}
var FormRows = React.createClass({
addRow: function() {
Model.add()
},
submit: function() {
alert(Model.values);
},
render: function() {
var rows = Model.values.map(function(value, index) {
return <FormRow key={index} onChange={this.onChange} index={index} value={value} />
}, this)
return (
<div>{rows}<button onClick={this.addRow}>Add row</button><button onClick={this.submit}>Submit form</button></div>
)
}
})
var FormRow = React.createClass({
onChange: function(e) {
Model.update(e.target.value, this.props.index)
},
render: function() {
return <input type='text' defaultValue={this.props.value} onChange={this.onChange} />
}
});
App = React.render(<FormRows />, document.body)
I used a simplified model/event example using Array and forceUpdate but the point here is to let the model "own" the form data. The child components can then make API calls on that model and trigger a re-render of the entire App with the new data (Flux).
Then just use the model data on submit.
Demo: http://jsfiddle.net/ekr41bzr/
Bind values of inputs to some model (for example build in Backbone or Flux) and on submit retrieve values from there, without touching inputs.

Categories