Redux: making sharing component trigger unique redux path depends on parent component - reactjs

Here is my scenario: I have a feed with list of questions. In each question, I have a upvote button for upvoting this question.
So I will design an upvote button component sharing between other question components. When clicking to this button, button will trigger an event to server. method signature sometime such as:
func upvote(questionOwnerId, upvoteUserId) { ... }
After upvoting, upvote will dispatch an event for redux handle and update state respectively. So upvote button will always display latest state. (how many upvoted users for that question).
This is my problem: I don't know how to make sharing component (upvote component), after calling method to server then trigger unique redux path on redux tree.
I have a solution for this: Upvote component will receive following parameters:
total number of upvote.
question's user id.
redux path.
Then upvote function and Reducer will use redux path for update state on redux tree respectively. Does this method works well and look clean for project ? Or is there any "true" way for my problem?
#Edit: Redux path for example: feeds.item.questionList.question[0] for question component at 0 index. feeds.item.questionList.question[1] for question component at 1 index ... so Reducer can understand how to update redux tree.

Such shared component can be done by just receiving the proper props and passing a generic onClick events.
For example, let say we create component named <Question /> and it will the props:
votes - to display current number of votes.
onUpvote - as a click event for the up-vote button.
onDownvote - as a click event for the down-vote button.
id - it needs the id in order to pass it to the parent's
function.
question - The value of the question to display.
Now, your parent component will take a list of questions from the redux-store and will .map on it and will return for each question a <Question /> component with the respective props (id, votes and question).
As for the onUpVote and onDownVote you will pass functions that created in the parent. These functions will dispatch actions and that will handled by your reducer which will return the new state, this will trigger a re-render with new data to show.
I've created a simple example, note that i can't use redux here so i managed the state inside the App component, but i mentioned in comments where you can dispatch actions and what logic should go inside the reducers.
const questionsList = [
{
id: 1,
votes: 2,
question: "whats up"
},
{
id: 2,
votes: -1,
question: "whats the time"
},
{
id: 3,
votes: 0,
question: "where are you"
},
{
id: 4,
votes: 7,
question: "who are you"
}
];
class Question extends React.Component {
constructor(props) {
super(props);
this.onUpvote = this.onUpvote.bind(this);
this.onDownvote = this.onDownvote.bind(this);
}
onUpvote() {
const { id, onUpvote } = this.props;
onUpvote(id);
}
onDownvote() {
const { id, onDownvote } = this.props;
onDownvote(id);
}
render() {
const { votes, question } = this.props;
const voteClassName = `votes ${votes < 0 ? 'low' : 'high'}`;
return (
<div className="question-wrapper">
<div className="buttons-wrapper">
<button className="button" onClick={this.onUpvote}>+</button>
<div className={voteClassName}>{votes}</div>
<button className="button" onClick={this.onDownvote}>-</button>
</div>
<div className="question">{question}</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
questions: questionsList
};
this.onUpvote = this.onUpvote.bind(this);
this.onDownvote = this.onDownvote.bind(this);
}
onUpvote(id) {
const { questions } = this.state;
// you can dispatch an action here
// and instead of doing this logic in here you can do it in your reducer
const nextState = questions.map(question => {
if (question.id != id) {
return question;
}
return {
...question,
votes: question.votes + 1
};
});
this.setState({ questions: nextState });
}
onDownvote(id) {
const { questions } = this.state;
// you can dispatch an action here
// and instead of doing this logic in here you can do it in your reducer
const nextState = questions.map(question => {
if (question.id != id) {
return question;
}
return {
...question,
votes: question.votes - 1
};
});
this.setState({ questions: nextState });
}
render() {
const { questions } = this.state; // get the questions via props (redux store)
return (
<div>
{questions.map(q => (
<Question
id={q.id}
question={q.question}
votes={q.votes}
onUpvote={this.onUpvote}
onDownvote={this.onDownvote}
/>
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.question-wrapper{
display: flex;
flex-direction: row;
width: 150px;
box-shadow: 0 0 2px 1px #333;
margin: 10px 0;
padding: 5px 20px;
}
.buttons-wrapper{
display:flex;
flex-direction: column;
}
.button{
margin: 10px 0;
cursor: pointer;
}
.question{
align-self: center;
margin: 0 20px;
font-size: 22px;
}
.high{
color: #3cba54;
}
.low{
color: #db3236;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Related

accessing state inside calculatestate from componentdidmount

I am new to flux and react and I would like to know how can I access state inside calculatestate() from
componentdidmount()?
how can I access events state inside componentdidmount()? I would like loop through events and then run a query per event, which in turn updates another store.
static getStores() {
return [SomeStore];
}
static calculateState(prevState, props) {
const somestore = SomeStore.getState();
return {
events: SomeStore.getState(),
};
}
componentdidmount(){
//this.state.events;
//need to do some query based on the events
//this query will update another store.
}
You need to use the lifecycle methods to understand the componentDidMount method which are outlined here
To initialise the localstate, you should use the constructor method (static can also be used as an alternative, but jsfiddle throws an error.
To show the initial state, you can log within componentDidMount with this.state.
const someStore = ["event"];
class TodoApp extends React.Component {
constructor(props) {
super(props);
this.state = { events: someStore };
}
// if you want to change state based on props
shouldComponentUpdate(nextProps, nextState) {
// change state if you want here
if (this.state.events.length !== nextState.events.length) {
console.log(this.state, nextState)
return true // update
}
return false
}
// get initial state
componentDidMount() {
console.log(this.state.events);
}
render() {
return (
<div>
<span>example</span>
<button onClick = {() => {
this.setState(state => {
return {
events: [...state.events, "new event"]
}
})
}}>Add Event</button>
<div>
events: { JSON.stringify(this.state.events) }
</div>
</div>
)
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"));
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>

setState not updating the state object

I'm learning react from the book full stack react. In one of the examples votingapp where you have products and a button to vote for the product. That button supposes to increase the number of votes for that product and I store the votes number in a component Parent's state and display it in a child component. That voting feature is not working.
I created Parent component where it displays child component that present product description, id, color and votes (the number of votes the product received)
import React, { Component } from 'react';
import './App.css';
import Product from "./Product";
var products = [
{
id: 1,
name: "one",
color: "blue",
votes:0
},
{
id: 2,
name: "two",
color: "green",
votes : 0
},
{
id: 3,
name: "three",
color: "Red",
votes : 1
}
];
class App extends Component {
//this function will be passed to child component so the child can pass any data needed back to the parent using function argument.
handleProductUpVote(productId) {
console.log("Product #" + " " +productId + " was upvoted")
};
render() {
const productComponents = products.map((product) => {
return <Product
key={"product-" + product.id}
id={product.id}
name={product.name}
color={product.color}
votes={product.votes}
/*this is how we pass the function from parent to the child as a prop*/
onVote={this.handleProductUpVote}
/>
});
return (
<div className="App">
{productComponents}
</div>
);
}
}
export default App;
and here is my child component where it renders the product details
import React, { Component } from 'react';
class Product extends Component {
constructor(props) {
super(props);
this.handleUpVote = this.handleUpVote.bind(this);
}
//using the function passed from parent in a child. We pass any data needed back to parent component using parent's function arugments
// invoke this function using onClick event inside the button
handleUpVote() {
this.props.onVote(this.props.id);
};
render() {
return (
<div>
<p> name: {this.props.name} </p>
<p> MYcolor: {this.props.color} </p>
{/*invoke the function using onClick so it will invoke handleUpVote that will update the prop and pass the data back to parent*/}
<button onClick={this.handleUpVote}> Upvote Me </button>
<p>{this.props.votes}</p>
<hr></hr>
</div>
)
}
};
export default Product;
this is working and I log to the console the message when I hit the button "Upvoteme"
But When I'm trying to move the setup to use state. It doesn't work Here is the parent component with the state and setState. When I click on the vote button nothing happens to the vote count.
import React, { Component } from 'react';
import './App.css';
import Child from "./Child";
var productseed = [
{
id: 1,
name: "one",
color: "blue",
votes: 0
},
{
id: 2,
name: "two",
color: "green",
votes : 0
},
{
id: 3,
name: "three",
color: "Red",
votes : 1
}
];
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
products: [],
};
this.handleProductUpVote = this.handleProductUpVote.bind(this);
}
componentDidMount() {
this.setState ({ products: productseed });
}
//this function will be passed to child component so the child can pass any data needed back to the parent using function argument.
handleProductUpVote(productId) {
// updating the vote in state
const nextProducts = this.state.products.map((product) => {
if (product.id === productId) {
return Object.assign({}, product, {
votes: product.votes + 1,
});
} else {
return product
}
});
this.setState({
products: nextProducts,
});
}
render() {
const productComponents = productseed.map((product) => {
return <Child
key={"product-" + product.id}
id={product.id}
name={product.name}
color={product.color}
votes={product.votes}
/*this is how we pass the function from parent to the child as a prop*/
onVote={this.handleProductUpVote}
/>
});
return (
<div className="App">
parent
{productComponents}
</div>
);
}
}
export default Parent;
The line below suppose to point to the products in the state but when I highlight it, it doesn't highlight the products in the this.state.
The consolelog is logging the message to my developer console. This is the issue, products in this.setState isn't pointing to the this.state.products and therefore not updating the state.
componentDidMount() {
this.setState({ products: productseed });
console.log("this products not pointing to the this.state.products, why?")
}
I read every question on stackoverflow related to setState not working and yet I have the same problem. If you an expert with react and able to take a look at this and figure out where is the issue, I would be so grateful. I'm unable to figure out when I assigned the this.setState({products: productseed}) and it doesn't update the state. I spent almost the past 4 hours reading and researching, please help.
Your problem lies in the render method of your Parent component. You're iterating over the productseeds array instead of your state. Since you're updating the state and not the seed array react sees no reason to rerender anything and therefore nothing changes.
So if you change that line from
const productComponents = productseed.map((product) => {...
to
const productComponents = this.state.products.map((product) => {...
you should be fine.
Moreover about your:
The line below suppose to point to the products in the state but when I highlight it, it doesn't highlight the products in the this.state.
This is just something related to the IDE you're using and nothing specific about react. You're passing an object with attributes and most IDEs (or all (?)) don't connect the combination with this.setState to the state object.

react counter for each item of list

I'm trying to create a counter for each item in a list in React. I want each to be incremented or decremented individually depending on what the user clicks on. This issue is that all counters increment and decrement on click
of a single element, but I would like only the clicked element's counter to change.
this is my code:
class name extends Component {
constructor(){
super()
this.state = {
news: [],
voteing: 0
}
}
onVoting(type){
this.setState(prevState => {
return {voteing: type == 'add' ? prevState.voteing + 1: prevState.voteing- 1}
});
}
render() {
return (
<React.Fragment>
<Content>
{
this.state.news.map((item, i)=>{
return (
<Item key={i}>
<text>
{item.subject}
{item.details}
</text>
<Votering>
<img src="" onClick={this.onVoting.bind(this, 'add')} />
<div value={this.state.voteing}>{this.state.voteing}</div>
<img src="" onClick={this.onVoting.bind(this, 'min')} />
</Votering>
</Item>
)
})
}
</Content>
</React.Fragment>
)
}
}
I'm trying to do this:
<img src="" onClick={this.onVote(i).bind(this, 'add')} />
but it doesn't work also tried this.onVote(item.i) and same result
I cannot really see how you would like to see voting as part of the local component state, as it really has to do (in my opinion), with the entities on which you can vote.
So if I were you, I would rewrite the code slightly different. As I do not know what you intend to do afterwards with the votes (this rather assumes like a live process, or at least a kind of save button, as it is saved here in the local VotingApp state), I just save everything to the local state, how you would handle that is not really my intend to answer.
So personally, I would rather go for one functional component, just rendering the news item and it's voting capability, where the voteCount is part of the item entity. If this is not how you receive the data, nothing stops you from adding the data after your fetch and before really showing it on the screen. The app itself will receive the changes and the item that will be changed, and what it does there-after, would be all up to you ;)
const { Component } = React;
const NewsItem = ( item ) => {
const { subject, details, voteCount, handleVoteChange } = item;
return (
<div className="news-item">
<div className="news-vote">
<div className="vote-up" title="Vote up" onClick={ () => handleVoteChange( item, 1 ) }></div>
<div className="vote-count">{ voteCount }</div>
<div className="vote-down" title="Vote down" onClick={ () => handleVoteChange( item, -1 ) }></div>
</div>
<div className="news-content">
<h3>{ subject }</h3>
<div>{ details }</div>
</div>
</div>
);
};
class VotingApp extends Component {
constructor( props ) {
super();
this.handleVoteChange = this.handleVoteChange.bind( this );
// by lack of fetching I add the initial newsItems to the state
// and work by updating local state on voteChanges
// depending on your state management (I guess you want to do something with the votes)
// you could change this
this.state = {
newsItems: props.newsItems
};
}
handleVoteChange( item, increment ) {
this.setState( ( prevState ) => {
const { newsItems } = prevState;
// updates only the single item that has changed
return {
newsItems: newsItems
.map( oldItem => oldItem.id === item.id ?
{ ...oldItem, voteCount: oldItem.voteCount + increment } :
oldItem ) };
} );
}
render() {
const { newsItems = [] } = this.state;
return (
<div className="kiosk">
{ newsItems.map( item => <NewsItem
key={ item.id }
{...item}
handleVoteChange={this.handleVoteChange} /> ) }
</div>
);
}
}
// some bogus news items
const newsItems = [
{ id: 1, voteCount: 0, subject: 'Mars in 2020', details: 'Tesla will send manned BFR rockets to Mars in 2020' },
{ id: 2, voteCount: -3, subject: 'Stackoverflow rocks', details: 'Stackoverflow is booming thanks to the new friendly policy' },
{ id: 3, voteCount: 10, subject: 'DS9: Healthy living', details: 'Eat rice everyday and drink only water, and live 10 years longer, says Dax to Sisko, Sisko suprises her by saying that like that, he doesn\'t want to live 10 years longer...' }
];
// render towards the container
const target = document.querySelector('#container');
ReactDOM.render( <VotingApp newsItems={ newsItems } />, target );
.kiosk {
display: flex;
flex-wrap: no-wrap;
}
.news-item {
display: flex;
justify-content: flex-start;
width: 100%;
}
.news-vote {
display: flex;
flex-direction: column;
align-items: center;
padding-left: 10px;
padding-right: 10px;
}
.news-vote > * {
cursor: pointer;
}
.news-content {
display: flex;
flex-direction: column;
}
.vote-up::before {
content: '▲';
}
.vote-down::before {
content: '▼';
}
.vote-up:hover, .vote-down:hover {
color: #cfcfcf;
}
h3 { margin: 0; }
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script id="prop-types" src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<div id="container"></div>
The reason all your items' counts change when any of them is clicked on is that they all share the same vote count value, voteing in the name component's state.
To fix this, you should break each item into its own stateful component. So that each can track its own click count.
For example:
class name extends Component {
constructor(){
super();
this.state = {
news: []
}
}
render() {
return (
<React.Fragment>
<Content>
{
this.state.news.map((item, i) => {
return <NewsItem key={ i }
subject={ item.subject }
details={ item.details }
/>
})
}
</Content>
</React.Fragment>
)
}
}
class NewsItem extends Component {
constructor() {
super();
this.state = {
voteCount = 0
}
}
handleVote(type) {
this.setState(prevState => ({
voteCount: type === "add" ? prevState.voteCount + 1 : prevState.voteCount - 1
}));
}
render() {
const { subject, details } = this.props;
const { voteCount } = this.state;
return (
<Item>
<text>
{ subject }
{ details }
</text>
<Votering>
<img src="" onClick={ this.handleVote.bind(this, 'add') } />
<div value={ voteCount }>{ voteCount }</div>
<img src="" onClick={ this.handleVote.bind(this, 'min') } />
</Votering>
</Item>
)
}
}
You could also maintain separate counts for each item within the parent component, but I find breaking into separate components to be much cleaner.
A few things I noticed unrelated to your question.
1) onVoting should be bound in your constructor or use onVoting = () => { ..... }
2) in your render function you have onVote instead of onVoting
On to your main question, in your state you are only maintaining one counter that is displayed and changed for all news elements. an easy way to get around this is to create a new react element for each news article that will handle the voting for each article.
class parent extends Component {
constructor(){
super()
this.state = {
news: null,
}
}
componentDidMount() {
// fetch data from api and minipulate as needed
this.setState({news: dataFromApi})
}
render() {
return (
<Content>
{
this.state.news.map((item, i)=>{
return (
<NewChildComponent data={item}/>
)
})
}
</Content>
)
}
}
class NewChildComponent extends Component {
constructor() {
super()
this.state = {
voting: 0,
}
}
onVoting = (e) => {
this.setState(prevState => ({
voteCount: e.target.name === "add" ? prevState.voteCount + 1 : prevState.voteCount - 1
}));
}
render () {
const {data} = this.props;
return (
<Item key={data.uniqueID}>
<text>
{data.subject}
{data.details}
</text>
<Votering>
<img src="" onClick={this.onVoting} name="add"/>
<div value={this.state.voteing}>{this.state.voteing}</div>
<img src="" onClick={this.onVoting} name="min"/>
</Votering>
</Item>
)
}
}
A little background on why you should not bind in your render function. https://medium.freecodecamp.org/why-arrow-functions-and-bind-in-reacts-render-are-problematic-f1c08b060e36
Here’s why: The parent component is passing down an arrow function on
props. Arrow functions are reallocated on every render (same story
with using bind). So although I’ve declared User.js as a
PureComponent, the arrow function in User’s parent causes the User
component to see a new function being sent in on props for all users.
So every user re-renders when any delete button is clicked. 👎
Also why you should not use an index as a key in React.
https://reactjs.org/docs/lists-and-keys.html
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
Here is an in-depth explanation about why keys are necessary if you’re
interested in learning more.

how can I select only one component of my array of component

this code here works but I don't know how to just click one of my component in >the array with this code I can change the color.
but I want to know how can I not change the color when I already change it in one >component thanks for the future answer
import React, { Component } from 'react';
export default class Seats extends Component {
constructor() {
super()
this.state = {
status: false,
};
}
changeColor(event) {
if (this.state.status === false) {
event.currentTarget.style.backgroundColor = '#D70202';
this.state.status = true;
}else {
this.state.status = false;
event.currentTarget.style.backgroundColor = '#0CB607';
}
}
render() {
let array = [];
for (var i = 0; i < 5; i++) {
array[i] = i;
}
const list = array.map((d, index) => <div className="seat" onClick={this.changeColor.bind(this)} key={index}></div>);
return (
<div>
{list}
</div>
);
}
}
.seat {
background-color: #0CB607;
border: 1px solid black;
height: 90px;
width: 90px;
}
There are two problems here, which need to be resolved separately:
Instead of using this.state.status = true|false you should use this.setState({ status: true|false }). This forces a re-render.
In your current approach, you are managing your state via just manipulating the DOM directly, setting the style.backgroundColor. This will get blown away the next time your component renders.
To address the second issue, I suggest storing the array of items you are manipulating as state at the component level. As an example:
JS:
export default class Seats extends React.Component {
constructor() {
super()
const seats = [...Array(5)].map(() => ({ status: false }))
this.state = { seats }
}
handleSeatClick(index) {
const seats = this.state.seats
const seat = seats[index]
seat.status = !seat.status
seats[index] = seat
this.setState({ seats })
}
render() {
return (
<div>{list.map((seat, index) =>
<div className={`seat ${seat.status ? 'highlight' : ''}`}
onClick={this.handleSeatClick.bind(index)}
></div>
</div>
)
}
}
CSS:
.seat {
background-color: #0CB607;
border: 1px solid black;
height: 90px;
width: 90px;
}
.seat.highlight {
background-color: #D70202;
}
In this example, we're persisting the array of seats in the component's state. If you are getting a pre-defined list of seats passed in, in the future, you could replace the line that creates the [...Array(5)]... bit with something that instead reads from props being passed in, or loads from an ajax call, etc.
Because the seats are persisted with their own state, as an array, we can simply inspect that state when rendering to determine whether to output the highlight CSS class - which applies the color.
One other thing you can refactor (which I didn't do, to keep this a clear explanation) is to get rid of the .bind in render entirely. Doing this is an anti-pattern, as it will re-create new functions for every item in the list, every time it renders.

How to preserve the state for a "just hidden" component

Through my previous question I learned that React preserves the state for child components automatically and that's why its documentation says:
What Shouldn't Go in State?
React components: Build them in render() based on underlying props and state.
Now my question is how to do the same for components that we are just hiding?
Consider a child component that in an iteration is not going to be shown and at the same time we would like to preserve its state for the future when it is going to be brought back!
To illustrate what I exactly mean I've designed a case scenario to show it to you. In the following code you can add stateful child components. A checkbox is provided for each component so you can flag them. Finally the third button will hide the child components that are not flagged. I'm looking for a way to restore the state for the not-flagged components once they are brought back.
Executable code
class BlackBox extends React.Component
{
constructor() {
super();
this.state = {
checked: false,
counter: 0,
};
}
increment = () => {
this.setState(Object.assign({}, this.state, { counter: this.state.counter+1 }));
};
switch = () => {
this.setState(Object.assign({}, this.state, { checked: !this.state.checked }));
};
isChecked() {
return this.state.checked;
}
render() {
return (
<span onClick={this.increment} title={this.props.id} style={{
fontSize: '24pt',
border: '1px solid black',
margin: 10,
padding: 10,
}}>
<input type="checkbox" onChange={this.switch} checked={this.state.checked} />
{this.state.counter}
</span>
);
}
}
class RedBox extends React.Component
{
constructor() {
super();
this.state = {
checked: false,
counter: 0
};
}
increment = () => {
this.setState(Object.assign({}, this.state, { counter: this.state.counter+1 }));
};
switch = () => {
this.setState(Object.assign({}, this.state, { checked: !this.state.checked }));
};
isChecked() {
return this.state.checked;
}
render() {
return (
<span onClick={this.increment} title={this.props.id} style={{
fontSize: '24pt',
border: '1px solid red',
margin: 10,
padding: 10,
}}>
<input type="checkbox" onChange={this.switch} checked={this.state.checked} />
{this.state.counter}
</span>
);
}
}
class Parent extends React.Component {
static blackCount = 0;
static redCount = 0;
state = {
childCmps: [],
showOnlyChecked: false,
};
constructor(props, context) {
super(props, context);
}
addBlackBox = () => {
this.setState(Object.assign({}, this.state, {
childCmps: [...this.state.childCmps, { Component: BlackBox, id: "black" + (++Parent.blackCount) }],
}));
};
addRedBox = () => {
this.setState(Object.assign({}, this.state, {
childCmps: [...this.state.childCmps, { Component: RedBox, id: "red" + (++Parent.redCount) }],
}));
};
showHide = () => {
this.setState(Object.assign({}, this.state, {
showOnlyChecked: !this.state.showOnlyChecked,
}));
};
render() {
let children = this.state.childCmps.map(child => <child.Component key={child.id} id={child.id} ref={child.id} />);
return (
<div>
<button onClick={this.addBlackBox}>Add Black Box</button>
<button onClick={this.addRedBox}>Add Red Box</button>
<button onClick={this.showHide}>Show / Hide Unchecked</button>
<br /><br />
{children.filter(box => !this.state.showOnlyChecked || this.refs[box.props.id].isChecked())}
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById("root")
);
The short answer:
There is no solution that only has advantages and no drawbacks (in real life as well as in your question).
You really have only 3 options here:
use parent state and manage your data (in parent component and/ or stores)
use child state and hide children you do not need (so find another animation solution)
use child state and accept that state is lost for children not re-rendered -
You may want to check out redux for using stores.
The long answer
If you want to
remove a child from the DOM (e.g. for animation purposes with ReactCSSTransitionGroup)
AND keep the checked/unchecked & counter info
Then you cannot keep this in child state. State is by definition bound to the lifecycle of a component. If it is removed from DOM (= unmounted), then you will loose all the state info.
So if you want to save this info, you have to move this info to the state of the parent component (for each child obviously).
You can find a working codepen here.
Some other notes about this code:
Your child <...Box> components now become pure = stateless components
child components call a method on the parent, to update checked state or counter increment. In these methods they pass their own ID
Better not to store entire components in state, but only the props that determine the components.
So the new parent state contains an array of objects, and each object has relevant prop data
In render of the parent: better to filter the components first, before creating an array of components to be rendered
In parent you need new methods (onCheck() and onClick()) that will update the specific object in the array from state. Requires some special fiddling to ensure that we do not directly mutate state here.
for setState(), you do not need to do an Object.assign() and pass in all of state again. If you provide an object with only the parameters that changed, react will leave the other parameters untouched.
rather than removing the component from the DOM entirely (as you are doing through filter, just hide it. Replace children.filter with the following:
{children.map((box, idx) => {
var show = !this.state.showOnlyChecked || this.refs[box.props.id].isChecked();
return <span key={idx} style={{display: (show? 'inline-block': 'none')}}>{box}</span>;
})}

Resources