CODE:
var React = require('react');
var Recipe = require('./Recipe.jsx');
var AddRecipe = require('./AddRecipe.jsx');
var EditRecipe = require('./EditRecipe.jsx');
var RecipeBox = React.createClass({
getInitialState: function () {
return {
recipesArray: [],
adding: false,
editing: false,
currentIndex: 0
};
},
handleClick: function () {
this.setState({
adding: true
});
},
handleEditClick: function(index) {
this.setState({
editing: true,
currentIndex: index
});
},
handleDeleteClick: function(index) {
var newRecipesArray = this.state.recipesArray;
newRecipesArray.splice(index-1,1);
this.setState({
recipesArray: newRecipesArray
});
},
handleClose: function() {
this.setState({
adding: false,
editing: false
});
},
handleAdd: function(newRecipe) {
this.setState({
recipesArray: this.state.recipesArray.concat(newRecipe)
});
},
handleEdit: function(newRecipe, index) {
var newRecipesArray = this.state.recipesArray;
newRecipesArray[index-1] = newRecipe;
this.setState({
recipesArray: newRecipesArray
});
},
render: function() {
var i = 0;
var that = this;
var recipes = this.state.recipesArray.map(function(item) {
i++
return (
<div key={"div"+i} className="table">
<Recipe key={i} name={item.name} ingredients={item.ingredients} />
<button key ={"edit"+i} onClick={() => { that.handleEditClick(i)}} className="btn edit btn-primary">Edit</button>
<button key ={"delete"+i} onClick={() => { that.handleDeleteClick(i)}} className="btn delete btn-danger">Delete</button>
</div>
);
});
return (
<div>
<h1>React.js Recipe Box</h1>
<button className="btn btn-primary" onClick={this.handleClick}>Add Recipe</button>
<table>
<tbody>
<tr>
<th>RECIPES</th>
</tr>
</tbody>
</table>
{recipes}
{ this.state.adding ? <AddRecipe handleClose={this.handleClose} handleAdd={this.handleAdd} /> : null }
{ this.state.editing ? <EditRecipe currentIndex = {this.state.currentIndex} handleClose={this.handleClose} handleEdit={this.handleEdit}/> : null }
</div>
);
},
});
module.exports = RecipeBox;
QUESTION:
How should I implement saving state to localStorage ?
What would be the most elegant implementation ?
Currently learning React and looking to write clean and elegant code.
Whenever an update to state is fired, it will trigger the lifecycle method of componentDidUpdate. You can hook into that method in order to save the state of the component.
componentDidUpdate() {
window.localStorage.setItem('state', JSON.stringify(this.state));
}
Depending on your use case, you should be able to load it back up on componentDidMount.
componentDidMount() {
// there is a chance the item does not exist
// or the json fails to parse
try {
const state = window.localStorage.getItem('state');
this.setState({ ...JSON.parse(state) });
} catch (e) {}
}
I would warn you, you probably want a solution more like redux with a localStorage adapter for a "full-fledged" solution. This one is pretty frail in a few different ways.
I would take a look at plugins that make localstorage easier (not browser specific). An example would be this:
https://github.com/artberri/jquery-html5storage
The page above has all the information you need to get started. If that one doesn't work then I would continue to search. There are plenty out there. There may be newer ones that use React as well. The jQuery plugins have worked for me when I was learning/doing Angular.
Related
CODE:
var RecipeBox = React.createClass({
getInitialState: function () {
return {
recipesArray: [],
adding: false,
editing: false,
currentIndex: 0
};
},
handleClick: function () {
this.setState({
adding: true
});
},
handleEditClick: function(index) {
this.setState({
editing: true,
currentIndex: index
});
},
handleClose: function() {
this.setState({
adding: false,
editing: false
});
},
handleAdd: function(newRecipe) {
this.setState({
recipesArray: this.state.recipesArray.concat(newRecipe)
});
console.log(this.state.recipesArray);
},
handleEdit: function(newRecipe, index) {
var newRecipesArray = this.state.recipesArray;
newRecipesArray[index-1] = newRecipe;
this.setState({
recipesArray: newRecipesArray
});
},
render: function() {
var i = 0;
var that = this;
var recipes = this.state.recipesArray.map(function(item) {
i++
that.handleEditClickSingle = that.handleEditClick.bind(this, i);
return (
<div className="table">
<Recipe key={i} name={item.name} ingredients={item.ingredients} />
<button key ={"edit"+i} onClick={that.handleEditClickSingle} className="btn edit btn-primary">Edit</button>
<button key ={"delete"+i} className="btn delete btn-danger">Delete</button>
</div>
);
});
return (
<div>
<h1>React.js Recipe Box</h1>
<button className="btn btn-primary" onClick={this.handleClick}>Add Recipe</button>
<table>
<tbody>
<tr>
<th>RECIPES</th>
</tr>
</tbody>
</table>
{recipes}
{ this.state.adding ? <AddRecipe handleClose={this.handleClose} handleAdd={this.handleAdd} /> : null }
{ this.state.editing ? <EditRecipe currentIndex = {this.state.currentIndex} handleClose={this.handleClose} handleEdit={this.handleEdit()}/> : null }
</div>
);
},
});
SITUATION:
Error:
Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
What have I done wrong ?
The crux of the issue seems to be here:
that.handleEditClickSingle = that.handleEditClick.bind(this, i);
SOLUTION:
var i = 0;
var that = this;
var recipes = this.state.recipesArray.map(function(item) {
i++
return (
<div className="table">
<Recipe key={i} name={item.name} ingredients={item.ingredients} />
<button key ={"edit"+i} onClick={() => { that.handleEditClick(i)}} className="btn edit btn-primary">Edit</button>
<button key ={"delete"+i} className="btn delete btn-danger">Delete</button>
</div>
);
});
I think your problem lies in the render method in the below line
{ this.state.editing ? <EditRecipe currentIndex = {this.state.currentIndex} handleClose={this.handleClose} handleEdit={this.handleEdit()}/> : null }
Replace it with
{ this.state.editing ? <EditRecipe currentIndex = {this.state.currentIndex} handleClose={this.handleClose} handleEdit={this.handleEdit}/> : null }
You are calling the this.handleEdit. Instead you should just pass it as above.
The code below is only working when I remove the componentWillMount that uses localStorage. With usage localStorage it gives a mistake
this.state.interests.map is not a function
I tried to move usage of localStorage out of component but it won't help. I suppose that using local storage somehow changes this.state.interests that they stop being an array.
let interests = ["Музыка", "Компьютеры", "Радио"]
let ListOfInterest = React.createClass({
getInitialState: function() {
return {value: '', interests: interests};
},
componentWillMount() {
let local = localStorage.getItem('interests')
if (local) {
this.setState({interests: local});
} else {
localStorage.setItem('interests', this.state.interests)}
},
deleteInterest(key) {
delete interests[key]
this.setState(this.state) // without this line the page will not re-render
},
addInterest() {
interests.unshift(this.state.value)
this.setState({value: ''})
},
handleChange(event) {
this.setState({value: event.target.value})
},
render() {
return <div className="interests">
<b>Интересы</b>
<br/>
{this.state.interests.map((int, index) => {
return <button onClick={() => {
this.deleteInterest(index)
}} key={index} className="btn-interest">{int}</button>
})}
<input type='text' placeholder="Add" value={this.state.value} onChange={(e) => this.handleChange(e)}/>
<button onClick={() => {
this.addInterest()
}} className="add">Add interest</button>
</div>
}
})
<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>
You have several issues in your example
in localStorage.setItem second argument have to be a String, you can not store Array(when you do it, in storage will be string separated by coma because called method toString - [1, 2, 3].toString() ), you need stringify array before set to Storage
keyValue A DOMString containing the value you want to give the
key you are creating/updating.
localStorage.setItem(
'interests', JSON.stringify(this.state.interests)
)
and parse when get value
let local = JSON.parse(localStorage.getItem('interests'));
this.setState(this.state) this is not good way to update state, you need update state like so
deleteInterest(key) {
this.setState({
interests: this.state.interests.filter((el, i) => i !== key)
})
},
addInterest() {
this.setState({
value: '',
interests: this.state.interests.concat(this.state.value)
});
},
Example
My React JS file is below:
The logic behind this:
1.) CreateTable renders CreateColumns, CreateRows, & ChangeResults
On the first render, CreateRows is empty, but once the component mounts, a fetch() call is made to update the rows, page re-renders and I have my table
2.) ChangeResultscomponent is loaded which creates an input box. State for num_of_rows to an empty string (placeholder, not sure if I even need to do this).
When I input some number into the input field and hit click, onClick runs updatePage, which calls the function updateRows (in CreateTable), that then changes the state of people_per_page. I have the console.log() to verify that this is actually happening, and it prints out as expected.
I thought that since CreateRows inherits people_per_page, and I'm changing the state of people_per_page, it would cause a re-render...but nothing is happening.
Can anyone see why that might be?
var CreateTable = React.createClass({
getInitialState: function(){
console.log('initial state loaded')
return {
'table_columns' : ['id','email', 'first', 'last', 'title', 'company', 'linkedin', 'role'],
people_per_page: 34
}
},
updateRows: function(rows) {
console.log(rows)
this.setState(
{people_per_page: rows},
function() {
console.log(this.state.people_per_page)
}
)
},
render: function(){
return (
<div>
<ChangeResults updateRows = {this.updateRows} />
<table>
<CreateColumns columns={this.state.table_columns} />
<CreateRows num_of_rows = {this.state.people_per_page} />
</table>
</div>
)
}
});
var ChangeResults = React.createClass({
getInitialState: function(){
return {
num_of_rows : ''
}
},
handleChange: function(e) {
this.setState({
'num_of_rows' : e.target.value
});
},
updatePage: function(){
this.props.updateRows(this.state.num_of_rows);
},
render: function(){
return (
<div>
Number of people per page: <br />
<input type="text" onChange = {this.handleChange} />
<button onClick={this.updatePage}> Update Page </button>
</div>
)
}
})
var CreateColumns = React.createClass({
render: function(){
var columns = this.props.columns.map(function(column, i){
return (
<th key={i}>
{column}
</th>
)
});
return (
<thead>
<tr>
{columns}
</tr>
</thead>
)
}
});
var CreateRows = React.createClass({
getInitialState: function() {
return {
'people':[],
}
},
componentDidMount: function(){
console.log('componentDidMount running')
this.createRow();
},
createRow : function(){
console.log('starting fetch')
fetch('http://localhost:5000/search', {
method: 'POST',
body: JSON.stringify({
people_per_page: this.props.num_of_rows
})
})
.then(function(response) {
return response.json()
})
.then((responseJson) => {
return this.setState({'people' : responseJson.people })
});
},
render: function(){
var rows = this.state.people.map(function(row, i){
return (
<tr key={i}>
<td>{row['id']}</td>
<td>{row['email']}</td>
<td>{row['first']}</td>
<td>{row['last']}</td>
<td>{row['title']}</td>
<td>{row['company']}</td>
<td>{row['linkedin_url']}</td>
<td>{row['role']}</td>
</tr>
)
})
return (
<tbody>
{rows}
</tbody>
)
}
});
ReactDOM.render(<CreateTable />, document.getElementById('content'));
In <CreateRows />, componentDidMount is only called for the first render, when the component is 'mounted' on the page. After that, you need to fetch new data in componentDidUpdate or somewhere else in the application.
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.
I am running through a react tutorial on tutsplus that is a bit old, and the code doesn't work as it was originally written. I actually am totally ok with this as it forces me to learn more independently, however I have spent a while on a bug that I just can't figure out. The bug consists of not being able to pass on an objects key, which prevents my program from updating the state of the correct object.
First off here is the repo if you want to run this code and see it in action: https://github.com/camerow/react-voteit
I have a child component that looks like this:
var FeedItem = React.createClass({
vote: function(newCount) {
console.log("Voting on: ", this.props, " which should have a key associated.");
this.props.onVote({
key: this.props.key,
title: this.props.title,
description: this.props.desc,
voteCount: newCount
});
},
voteUp: function() {
var count = parseInt(this.props.voteCount, 10);
var newCount = count + 1;
this.vote(newCount);
},
voteDown: function() {
var count = parseInt(this.props.voteCount, 10);
var newCount = count - 1;
this.vote(newCount);
},
render: function() {
var positiveNegativeClassName = this.props.voteCount >= 0 ?
'badge badge-success' :
'badge badge-danger';
return (
<li key={this.props.key} className="list-group-item">
<span className={positiveNegativeClassName}>{this.props.voteCount}</span>
<h4>{this.props.title}</h4>
<span>{this.props.desc}</span>
<span className="pull-right">
<button id="up" className="btn btn-sm btn-primary" onClick={this.voteUp}>↑</button>
<button id="down" className="btn btn-sm btn-primary" onClick={this.voteDown}>↓</button>
</span>
</li>
);
}
});
Now when someone hits the vote button the desired behavior is for the FeedItem.vote() method to send an object up to the main Feed component:
var FeedList = React.createClass({
render: function() {
var feedItems = this.props.items;
return (
<div className="container">
<ul className="list-group">
{feedItems.map(function(item) {
return <FeedItem key={item.key}
title={item.title}
desc={item.description}
voteCount={item.voteCount}
onVote={this.props.onVote} />
}.bind(this))}
</ul>
</div>
);
}
});
Which should pass that key on throught the parent component's onVote function:
var Feed = React.createClass({
getInitialState: function () {
var FEED_ITEMS = [
{
key: 1,
title: 'JavaScript is fun',
description: 'Lexical scoping FTW',
voteCount: 34
}, {
key: 2,
title: 'Realtime data!',
description: 'Firebase is cool',
voteCount: 49
}, {
key: 3,
title: 'Coffee makes you awake',
description: 'Drink responsibly',
voteCount: 15
}
];
return {
items: FEED_ITEMS,
formDisplayed: false
}
},
onToggleForm: function () {
this.setState({
formDisplayed: !this.state.formDisplayed
});
},
onNewItem: function (newItem) {
var newItems = this.state.items.concat([newItem]);
// console.log("Creating these items: ", newItems);
this.setState({
items: newItems,
formDisplayed: false,
key: this.state.items.length
});
},
onVote: function (newItem) {
// console.log(item);
var items = _.uniq(this.state.items);
var index = _.findIndex(items, function (feedItems) {
// Not getting the correct index.
console.log("Does ", feedItems.key, " === ", newItem.key, "?");
return feedItems.key === newItem.key;
});
var oldObj = items[index];
var newItems = _.pull(items, oldObj);
var newItems = this.state.items.concat([newItem]);
// newItems.push(item);
this.setState({
items: newItems
});
},
render: function () {
return (
<div>
<div className="container">
<ShowAddButton displayed={this.state.formDisplayed} onToggleForm={this.onToggleForm}/>
</div>
<FeedForm displayed={this.state.formDisplayed} onNewItem={this.onNewItem}/>
<br />
<br />
<FeedList items={this.state.items} onVote={this.onVote}/>
</div>
);
}
});
My logic relies on being able to reconcile the keys in the onVote function, however the key prop is not being properly passed on. So my question is, how do I pass on they key through this 'one way flow' to my parent component?
Note: Feel free to point out other problems or better design decision, or absolute stupidities. Or even that I'm asking the wrong question.
Looking forward to a nice exploration of this cool framework.
The key prop has a special meaning in React. It is not passed to the component as a prop, but is used by React to aid the reconciliation of collections. If you know d3, it works similar to the key function for selection.data(). It allows React to associate the elements of the previous tree with the elements of the next tree.
It's good that you have a key (and you need one if you pass an array of elements), but if you want to pass that value along to the component, you should another prop:
<FeedItem key={item.key} id={item.key} ... />
(and access this.props.id inside the component).