Reputation: 310
I have an app with multiple Views
. Each View
has some number of mapped child Entities
. Each of those entities has a state for whether or not it's collapsed/hidden called: this.state.show
. Each Entity
has a button that toggles the show
state. That all works fine.
What I'd like to do is add a button up on the View
that shows or hides all Entities
at the same time. I thought this would be simple, but there are several complicating factors:
Views
in the app each displaying the same data in different ways, so storing show
in the lifted up prop isn't much of an option. I also don't want to store UI state in the file data, because that rubs me the wrong way.Entity
as a state rather than a prop of the View
Entities
will change frequently. This is why I don't want to store the state of the children in the parent, because if I do and the entities change, then I have to somehow translate that change in props to a change in state, which is strongly cautioned against.View
to another.What's the right strategy to pursue here? Am I not thinking "react" enough?
import React, { Component } from 'react'
import Entity from './Entity.jsx'
import Filter from './Filter.jsx'
class View extends Component {
render() {
return (
<div className="view main">
{this.props.entities ? this.props.entities.map(
(e, i) => (
<Entity
entity={e}
propertyTypes={this.props.propertyTypes}
key={i}
index={i}
taglists={this.props.taglists}
ee={this.props.ee}
/>
)
) : ""}
</div>
)
}
}
export default View
import React, { Component } from 'react'
import Property from './Property.jsx'
import Event from './Event.jsx'
import Relationship from './Relationship.jsx'
class Entity extends Component {
constructor (props) {
super(props);
this.state = {
show: true
}
this.showHide = this.showHide.bind(this)
}
showHide() {
this.setState({
show: !this.state.show
})
}
render() {
return (
<div className="entity" id={this.props.entity.name}>
<div className="entity-header">
<h2>
<input
type="text"
value={this.props.entity.name}
onChange={this.emit}
data-original={this.props.entity.name}
data-index={this.props.index}
data-event="rename"
></input>
</h2>
<button
className="entity-collapse"
data-index={this.props.index}
onClick={this.showHide}
>{this.state.show ? "-" : "+"}</button>
</div>
<div className={this.state.show ? "entity-content" : "entity-content hidden"}>
...
removed for clarity
...
</div>
</div>
)
}
}
export default Entity
Upvotes: 0
Views: 73
Reputation: 1917
Your state should only be in the parent component, not any child components. You would then pass your functions and state as props to the children.
Upvotes: 2
Reputation: 24181
Below is an example of storing state in a higher order component.
If you look at the code there is just one place where state is stored, and that's in the View
, This is known as single source of truth, and it makes coding in React much simpler.
For example if you click the all
checkbox, all will check. And if you individually check each one, the all
and none
will also keep in sync.
You can of course alter this to suite you needs, I've used checkbox here as it's easy to see the state changes working.
For larger applications the single source of truth method becomes even more important, and storing state inside React is best avoided. This is what Redux is known for, like I mentioned in comments Redux is not something I use personally, but rolled my own single source of truth state management using proxy's, if I get time I might create an NPM module using this, as I'm sure other users would find it useful.
ps,. I've used React Hooks in the example as I believe it's the future of React, but it would be trivial to alter to use classes..
const {useState, cloneElement} = React;
function MultiCheck(props) {
const {checkallto, list, setList} = props;
const checked = !list.some(f => f !== checkallto);
return <div>
<input
onClick={() => {
setList(new Array(list.length).fill(checkallto));
}}
onChange={() => {}}
checked={checked}
type="checkbox"/>
{props.children}
</div>;
}
function View(props) {
const [list, setList] = useState(new Array(props.children.length).fill(false));
function setEntityChecked(ix) {
return (check) => {
const newlist = [...list];
newlist[ix] = check;
setList(newlist);
};
}
const children = props.children.map((
f, ix) => cloneElement(f, {key:ix, checked: list[ix],
setChecked: setEntityChecked(ix)}));
return <div>
<MultiCheck checkallto={true} list={list} setList={setList}>
All
</MultiCheck>
<MultiCheck checkallto={false} list={list} setList={setList}>
None
</MultiCheck>
<hr/>
{children}
</div>;
}
function Entity(props) {
const {checked, setChecked} = props;
return <div><input
checked={checked}
onChange={()=>{}}
onClick={() => setChecked(!checked)}
type="checkbox"/>{props.children}</div>;
}
ReactDOM.render(<React.Fragment>
<View>
<Entity>One</Entity>
<Entity>Two</Entity>
<Entity>Two</Entity>
</View>
</React.Fragment>, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="mount"></div>
Upvotes: 0