Reputation: 49
The problem I ran into is this: I want to have multiple buttons that open the same modal, and depending on which button is pressed, the modal will have different content.
The buttons are placed in a Component and the modal is placed in another one (also, they are not in a parent-child relationship).
I have a "state object" that saves info about which button was clicked and whether the modal is opened or closed. This object needs to be read AND modified by BOTH components.
In Angular, I would create a Service that manages the state of the buttons and the modal by injecting the "state object" into both components.
How would I solve this in React?
(I read somewhere that this can be accomplished by using Redux, but at the moment my app doesn't implement the flux architecture and I'd need a solution that doesn't require flux/redux)
Upvotes: 2
Views: 2602
Reputation: 571
The short answer - you can't do this w/o holding state on in the top-level component, this is a limitation due to the fact that React offers parent-child connection only.
There is a lot of different solutions addressed to fix this issue. People use containers like react-redux
to share the common state. The problem with them is that you still has to do some work and answer some questions on how to compose the state and connect it.
This is why I made a more general solution which allows you to "mount" component even from different branch of three. The solution feels like a symlink in filesystem. More detailed problem definition, src and examples can be found here: https://github.com/fckt/react-layer-stack#rationale
Rationale
react
/react-dom
comes comes with 2 basic assumptions/ideas:
- every UI is hierarchical naturally. This why we have the idea of
components
which wrap each otherreact-dom
mounts (physically) child component to its parent DOM node by defaultThe problem is that sometimes the second property isn't what you want in your case. Sometimes you want to mount your component into different physical DOM node and hold logical connection between parent and child at the same time.
Canonical example is Tooltip-like component: at some point of development process you could find that you need to add some description for your
UI element
: it'll render in fixed layer and should know its coordinates (which are thatUI element
coord or mouse coords) and at the same time it needs information whether it needs to be shown right now or not, its content and some context from parent components. This example shows that sometimes logical hierarchy isn't match with the physical DOM hierarchy.
Take a look at https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example to take a look at the concrete example (you're able to use logically top-level state inside the Layer):
import React, { Component } from 'react';
import { Layer, LayerContext } from 'react-layer-stack';
import FixedLayer from './demo/components/FixedLayer';
class Demo extends Component {
render() {
return (
<div>
<Layer use={ [this.state.counter] } id="lightbox2">{ (_, content) =>
<FixedLayer style={ { marginRight: '15px', marginBottom: '15px' } }>
{ content } { this.state.counter }
</FixedLayer>
}</Layer>
<LayerContext id="lightbox2">{({ showMe, hideMe }) => (
<button onMouseLeave={ hideMe } onMouseMove={ ({ pageX, pageY }) => {
showMe(
<div style={{
left: pageX, top: pageY + 20, position: "absolute",
padding: '10px',
background: 'rgba(0,0,0,0.7)', color: '#fff', borderRadius: '5px',
boxShadow: '0px 0px 50px 0px rgba(0,0,0,0.60)'}}>
“There has to be message triage. If you say three things, you don’t say anything.”
</div>)
}}>Yet another button. Move your pointer to it.</button> )}
</LayerContext>
</div>
)
}
}
Upvotes: 1
Reputation: 53169
One of the core ideas of Redux is to have a global state and only components very high up in the DOM tree will connect to it. You can simulate this idea in React without Redux by shifting the state higher up into the component tree, and this state will essentially become your "Redux store".
state
in that component. There definitely will be a common ancestor (the root) for every pair of components which are not root elements.props
so that these child components can call the actions and modify the state within the ancestor.Some example code is shown below. Let me know if you need further clarification.
class Ancestor extends React.Component {
constructor(props) {
super(props);
this.state = { object: null };
}
someMethod(data) {
this.setState({
object: data
});
}
render() {
return (
<div>
<div>
<Button clickHandler={this.someMethod.bind(this)}/>
<Button clickHandler={this.someMethod.bind(this)}/>
</div>
<Modal data={this.state.object} someProp={this.someMethod.bind(this)}/>
</div>
)
}
}
class Button extends React.Component {
doSomething() {
this.props.clickHandler(someData);
}
render() {
return (
<button onClick={this.doSomething.bind(this)}/>Button</button>
);
}
}
Upvotes: 3
Reputation: 3415
If you can't use Redux the only solution I see is to pass model to all components. Your data should be stored as separate object (model) which implements all possible changes user can take. e.g.
app.model = {
addItem: function() { .. }
update: function() { .. }
}
Then you can pass this object as a props to your components.
<MyButton model={app.model}>Add Item</MyButton>
var MyButton = React.createClass({
render: function() {
return (
<div className="coolButton" onClick={this.props.model.addItem}>
{this.props.children}
</div>
})
Information if modal is opened or not you should keep in state of modal or its parent.
var MyModal = React.createClass({
getInitialState: function() {
return {isShown: false};
},
render: function() {
if (this.state.isShow===false) {
return
}
return (
<div className="modal">
{this.props.children}
</div>
})
Upvotes: 2
Reputation: 7308
Normally your case is a perfect case for using redux. But since you need a non-flux/redux solution, you should take a look at how to use React context Link to Context docs
Occasionally, you want to pass data through the component tree without having to pass the props down manually at every level. React's "context" feature lets you do this.
Only requirement to use Context is that it needs a wrapper parent component. If you don't have one, you need to add one. Wrapper component in your route could solve this.
Upvotes: 2