Communicate with parent component without using state/props - javascript

I need a parent component to know when it's child components have changed from collapsed to expanded and viceversa.
I don't want to keep that in the app state (Redux). I can't move the logic up to the parent component.
My components are:
I have component C which is an item that can be expanded/collapsed.
Then I have Component B which is a wrapper for component C in a specific case (gives it the draggable behaviour).
And finally component A which lists components C in a loop. A, sometimes wraps Cs in B, and sometimes not (when items shouldnt be draggable).
I need B to know C is expanded, so that it isn't draggable while expanded.
I can't put the expanded/collapsed logic in B, because C should always be collapsible/expandable independently of if draggable.
Is there any simple way to accomplish this without needing to have the expand/collapse state of each item in the app state?
I have read about https://facebook.github.io/react/docs/context.html but seems still in experimental phase...

You can keep the expanded state inside the C component and update the draggable state of the B component with a callback passed with the props of the C component.
In this way both components keep their own state, no need to add the state on the A component or in you App state (the Redux one).
Here is an example: https://jsfiddle.net/snahedis/69z2wepo/28567/
var sampleList = [{id: 1, draggable: true},{id: 2, draggable: false}];
var Acomponent = React.createClass({
render: function() {
return (
<div>
{sampleList.map(function(component) {
if (component.draggable) {
return <Bcomponent key={component.id} />;
} else {
return <Ccomponent key={component.id} />;
}
})}
</div>
);
}
});
var Bcomponent = React.createClass({
getInitialState: function() {
return {
draggable: true
}
},
hasBeenToggled: function() {
this.setState({
draggable: !this.state.draggable
});
},
render: function() {
return <Ccomponent draggable={this.state.draggable} toggleExpandableCallback={this.hasBeenToggled} />;
}
});
var Ccomponent = React.createClass({
getInitialState: function() {
return {
expanded: false
}
},
toggleExpandable: function() {
this.setState({
expanded: !this.state.expanded
});
if (typeof this.props.toggleExpandableCallback === "function") {
this.props.toggleExpandableCallback();
}
},
render: function() {
return (
<div>
<div>I'm the C component and my expanded state is : {this.state.expanded.toString()}</div>
<div>{(this.props.draggable) ? "oh, and I'm draggable !" : "and I'm saddly not draggable"}</div>
<button onClick={this.toggleExpandable}>Change expandable state</button>
</div>
);
}
});
ReactDOM.render(
<Acomponent />,
document.getElementById('container')
);

A few ideas:
1) You can ALWAYS render B (A renders B's, each which wraps a C), but just disable the draggable behavior in B when it shouldn't be draggable (pass it in something like a isDraggable to B). Then, C's collapsible state can be stored in B, and passed a prop to C, as B will always be there.
2) You can move all the state to A (not sure if this is what you mean by "app state")
It's hard to suggest without knowing more context. But in cases like this, there's almost a better way to break up your components / app state so that it doesn't feel "wrong."

Related

Reactjs - parent, child action and functions

I am working on a reactjs application - and I am breaking up a big component to have a child component. I've created a callback function in the child that will go back to the parent. When a checkbox is checked -- the child component does the callback and this goes back into the parent shell -- however I want to now jump out of the event callback and push the data to an original parent function.
my application kind of looks like this on a streamlined level.
var Parent = React.createClass({
onSelect: function(value, flag){
this.updateSelected(value, flag);
}
updateSelected: function(value, flag) {
let array = this.state.selectedArray;
array.push({"value": value, "flag": flag});
this.setState({
selectedArray: array
});
},
render: function() {
return (
<div>
<Child onSelect={this.changeHandler} />
<span>{this.state.value}</span>
</div>
);
}
});
var Child = React.createClass({
selectHandler: function(e) {
this.props.onSelect(e.target.value, false);
},
render: function() {
return (
<input type="checkbox" onSelect={this.selectHandler} />
);
}
});
but I can not just invoke the "this.updateSelected(value, flag);" as its inside the this scope.
You should use ES6 syntax and JSX syntax instead of pure React API, that help you a lot to reduce this kind of error about scoping and become your code more readable, but If you even want to use that syntax, so you should bind the function once you pass down to the child component , you can do it like this:
var Parent = React.createClass({
onSelect: function(value, flag){
this.updateSelected(value, flag);
}
updateSelected: function(value, flag) {
let array = this.state.selectedArray;
array.push({"value": value, "flag": flag});
this.setState({
selectedArray: array
});
},
render: function() {
return (
<div>
<Child onSelect={this.changeHandler.bind(this)} />
<span>{this.state.value}</span>
</div>
);
}
});
as you can see adding the .bind method and passing as argument the context that you want your function execute, then once your function be invoked, the scope of that function will be the parent component instead of child component.
Remember that this approach bind method could affect the improve of your component if your Parent component rerenders many times.
If you are using JSX syntax you should do something like
class YourComponent extends React.Component {
changeHandler = (value, flag) => {
this.updateSelected(value, flag);
}
render() {
return (
<div>
...
<Child onSelect={this.changeHandler} />
</div>
);
}
}

Reusing react component

I'm with some questions on the use of component react.
Basically I want to use the "Title" component in various parts of the code, but it always has "states" that vary. I do not quite understand the official documentation on this issue, as I do to inherit this component, and only change the "states" for what I want?
I know the question seems silly, but I'm learning and React is very different from everything I saw.
var Title = React.createClass({
displayName: "Title",
getDefaultProps: function () {
return {
className: ""
}
},
render: function () {
return <h1 className={this.props.className}>{this.state.content}</h1>
}
});
You should use props rather than state in this instance. Props are owned by parents, and state is owned by the component itself. Since you would like to use this component in multiple places, then naturally a parent will decide what the text of the Title should be - therefore props are what you want.
React has a special prop children, which takes on the value of whatever is passed inside a component's JSX tag.
For instance, here's a mock component which uses your Title component multiple times:
var MyComponent = React.createClass({
render: function() {
return (
<div>
<Title className="foo">Hello</Title>
<Title className="bar">World</Title>
</div>
);
}
});
Since you are now passing text to the component as the children prop, you must update you Title component to render this:
var Title = React.createClass({
displayName: "Title",
getDefaultProps: function () {
return {
className: ""
}
},
render: function () {
// NOTE: we are now using children prop
return <h1 className={this.props.className}>{this.props.children}</h1>
}
});
A side benefit of this is that you can build more complex titles, containing multiple children, and it will just work:
var MyOtherComponent = React.createClass({
render: function() {
return (
<div>
<Title className="foo">
<span>Hello</span>
World
</Title>
</div>
);
}
});
You might want to replace {this.state.content} by {this.props.children} and use your component like this :
<Title className="myclass">my title</Title>
As a general rule, try to avoid using state when possible, dumb component are more easily reusable.

If the props for a child component are unchanged, does React still re-render it?

Suppose I have the following pairing of parent and child components in React:
var ChildComponent = React.createClass({
getDefaultProps: function(){
return {
a: 'a',
b: 'b',
c: 'c'
}
},
render: function() {
return (
/* jshint ignore:start */
<div className={'child' + (this.props.b ? ' modifierClass' : '')} something={this.props.a}>
{this.props.c}
</div>
/* jshint ignore:end */
);
}
});
var ParentComponent = React.createClass({
componentDidMount: function(){
//After 10 seconds, change a property that DOES NOT affect the child component, and force an update
setTimeout(function(){
this.setState({foo: 'quux'});
this.forceUpdate();
}.bind(this), 10000);
}
getInitialState: function(){
return {
foo: 'bar',
a: 1,
b: 2,
c: 3
}
},
render: function() {
return (
/* jshint ignore:start */
<div className="parent">
<ChildComponent a={this.props.a} b={this.props.b} c={this.props.c}/>
</div>
/* jshint ignore:end */
);
}
});
React.render(
/* jshint ignore:start */
<ParentComponent />,
/* jshint ignore:end */
document.getElementsByTagName('body')[0]
);
When I do the forceUpdate, since none of the props that were passed to the ChildComponent changed, will React try to re-render it? What if I have 1000 such children, etc?
What I'm worried about is a situation where I have a very deep ChildComponent containing a whole massive tree of descendant components, but I only want to enact some relatively cosmetic changes on the ParentComponent. Is there any way to get React to update only the parent, without trying to re-render the children as well?
When React re-renders ParentComponent it will automatically re-render ChildComponent. The only way to get around is to implement shouldComponentUpdate in the ChildComponent. You should compare this.props.a, this.props.b and this.props.c and ChildComponents own state to decide to re-render or not. If you are using immutable data than you can just compare previous and next state and props using strict equality ===.
There are a few thing to note with your code
You dont need to forceUpdate when you setState. React automatically does it for you.
You probably meant:
<ChildComponent a={this.props.a} b={this.props.b} c={this.props.c}/>

Clone a component that has already been mounted

I am trying to build an app that uses drag-and-drop behaviour, and the component being dragged needs to be cloned elsewhere in the DOM. Since the component is already mounted, trying to mount it again causes the browser to hang.
Trying to use cloneWithProps results in a Cannot read property 'defaultProps' of undefined error.
Here's a testcase:
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem} />
{this.state.draggingItem}
</div>
},
setDraggingItem (component) {
// This gives `Cannot read property 'defaultProps' of undefined`
//React.addons.cloneWithProps(component)
// This crashes the browser
//this.setState({ draggingItem: component })
}
})
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
handleOnClick (event) {
this.props.onClick(this)
}
})
React.render(<TestCase />, document.body)
Of course I could simply clone component.getDOMNode() in setDraggingItem, but it really seems like rendering the component or calling cloneWithProps should work?
The two things you need to create an element is: the component class (e.g. ExampleComponent) and its props. cloneWithProps is only to be used in render and only with an element coming from props which was created in another component's render. You shouldn't save elements, or pass them around other than to other components in render. Instead, you pass around objects (props) and component classes.
Since you need to know the props and component class to render it in the first place, you can handle all of this in TestCase.
var TestCase = React.createClass({
getInitialState () {
return {
draggingItem: null,
draggingItemProps: null
}
},
render () {
return <div>
<ExampleComponent onClick={this.setDraggingItem.bind(null,
/* the component class */ ExampleComponent,
/* the props to render it with */ null
)} />
{
this.state.draggingItem && React.createElement(
this.state.draggingItem,
this.state.draggingItemProps
)
}
</div>
},
setDraggingItem (component, props, event) {
this.setState({ draggingItem: component, draggingItemProps: props })
}
});
var ExampleComponent = React.createClass({
render () {
return <div onClick={this.handleOnClick}>Hello World</div>
},
// just defer the event
handleOnClick (event) {
this.props.onClick(event)
}
});
If you wish to make these valid outside this TestCase component, ensure there aren't any functions bound to TestCase in the props. Also ensure there's no children prop with react elements in it. If children are relevant, provide the {componentClass,props} structure needed to recreate them.
It's hard to tell what your actual requirements are, but hopefully this is enough to get you started.
You need be sure you're creating a new component with the same props, not mount the same one multiple times. First, setup a function that returns an instantiated components (easier to drop JSX here):
function getComponent(props) {
return ExampleComponent(props);
}
Then in your TestCase render:
return (<div>
{ getComponent({ onClick: this.setDraggingItem }) }
{ this.state.draggingItem }
</div>);
That will create the first component. Then to create a clone:
setDraggingItem(component) {
var clone = getComponent(component.props);
}
This deals with the cloning part. You still have some dragging and rendering to figure out.

Set inline style of children

I’m trying to set some inline styles to children in React. I have this that actually works, sort of:
var App = React.createClass({
render: function() {
var children = React.Children.map(this.props.children, function(child, i) {
child.props.style.width = '100px'
})
return (
<div>{children}</div>
)
}
})
BUT, if I try to change styles when state changes, the styles will remain the same. You can easily see this when trying something like this: (fiddle: http://jsfiddle.net/9UvWL/)
var World = React.createClass({
getInitialState: function() {
return { width: 0 }
},
componentDidMount: function() {
this.setState({ width: 100 })
},
componentDidUpdate: function() {
console.log(this.getDOMNode().innerHTML);
},
render: function() {
var width
var children = React.Children.map(this.props.children, function(child, i) {
width = i*this.state.width
console.log('Setting width: '+width);
child.props.style = {width: (i*this.state.width)+'px'}
return child
}, this)
return <div>{children}</div>
}
})
var Hello = React.createClass({
render: function() {
return (
<World>
<div>1</div>
<div>1</div>
</World>
)
}
})
React.renderComponent(<Hello />, document.body);
It will log that width has changed, but the props.style is not working as expected. And using child.setProps() will throw "Invariant Violation: replaceProps(...): Can only update a mounted component.". Is there another "react-way" of changing inline styles to children?
You are modifying the props object of each child directly, which bypasses parts of React's update mechanisms. We will be making this impossible soon.
In this case you want to use cloneWithProps inside your World component's render function.
var children = React.Children.map(this.props.children, function(child, i) {
width = i*this.state.width;
console.log('Setting width: '+width);
return React.addons.cloneWithProps(child, {style: {width: width + 'px'}})
}, this)
return <div>{children}</div>
cloneWithProps creates a new copy of the component and merges in additional props. You should use this pattern to ensure you're giving React all the hints it needs to know when to update.
Here's your example working: http://jsfiddle.net/zpao/Tc5Qd/
Just after the "Can only update a mounted component" check, there is another more descriptive error message:
invariant(
this._mountDepth === 0,
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
'component with a parent. This is an anti-pattern since props will ' +
'get reactively updated when rendered. Instead, change the owner\'s ' +
'`render` method to pass the correct value as props to the component ' +
'where it is created.'
);
The logic comes from a different perspective than updating DOM nodes. When a DOM node needs to be changed, the way to do that normally is to get a reference to the DOM node, then set its properties. With React, instead of updating particular nodes, the render methods just return new React components with the correct properties set, then the back-end code works out which DOM nodes to update.
When something is shown wrongly in normal DOM code, it is hard to know where the problem is, since any code can update the DOM nodes.
When the code is arranged into render methods like in React, the render methods always take in this.props and this.state and return entire, fully rendered, components. Then, if something isn't rendered properly, it is always possible to look in the render method to find the problem, since that is the only place where that render happens. (And this works whether it is the first render or a second render, so there doesn't need to be a distinction between rendering components the first time, and updating components.)
So, the way around the problem you're describing, is to move the logic about rendering the divs into the render method which creates the divs, rather than in the render method of a different component. Something like this:
var World = React.createClass({
render: function() {
return <div>{this.props.children}</div>;
}
});
var Hello = React.createClass({
getInitialState: function() {
return { width: 0 };
},
componentDidMount: function() {
this.setState({ width: 100 });
},
render: function() {
var children = ["a", "b", "c"].map(function(content, i) {
return (
<div style={{ width: i * this.state.width }}>
{content}
</div>
);
}, this);
return (
<World>
{children}
</World>
)
}
});
React.renderComponent(<Hello />, document.body);
http://jsfiddle.net/kN4DV/2/

Categories