I'm creating so called recipe box, where you should be able to add/edit/delete recipes. Initial rendering part seems to be working fine, but I'm struggling when it comes to states and updating html depending what was changed: whether existing recipe was modified, deleted or new one added.
Currently I implemented state change trigger when recipe is edited. By reading various articles I came to conclusion that if you want to read values from another element when some other element is interacted (in my case from input element when button element is clicked), I need to add state to track input directly while it is typed and then use that state to trigger what I want (In my case I just use value from so called pending state and set to normal state when that button is pressed).
But it seems it is not working. Though I'm probably doing something wrong.
Here is the part I implemented states I talked about:
class RecipeComponent extends React.Component {
constructor(props){
super(props);
this.state = {
title: '',
pendingTitle: '',
ingredients: '',
pendingIngredients: '',
}
}
handleChange(e, key){
let obj = {};
obj[key] = e.target.value;
this.setState(obj);
}
handleClick(){
this.setState(
{title: this.pendingTitle, ingredients: this.pendingIngredients});
}
_renderModal(target, ctx){
return (
<div className="modal fade" id={target} role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal">×</button>
<h4 className="modal-title">{ctx.title}</h4>
</div>
<div className="modal-body">
<div className="form-group">
<label htmlFor="title" className="control-label"><span>Recipe</span></label>
<input type="text" id="title" className="form-control" placeholder="Recipe Name" defaultValue={ctx.recipeTitle ? ctx.recipeTitle : ''}
onKeyUp={(e) => this.handleChange(e, 'pendingTitle')}
/>
</div>
<div className="form-group">
<label htmlFor="ingredients" className="control-label"><span>Ingredients</span></label>
<input type="text" id="ingredients" className="form-control" placeholder="Enter Ingredients, separated by commas" defaultValue={ctx.ingredients ? ctx.ingredients : ''}
onChange={(e) => this.handleChange(e, 'pendingIngredients')}
/>
</div>
</div>
<div className="modal-footer">
{/*Seems to not update state properly*/}
<button type="button" className="btn btn-primary" onClick={() => this.handleClick()} data-dismiss="modal">{ctx.title}</button>
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
);
}
...
...
{/*Here title from state is never set*/}
// Should this.state.title replace default title?
recipeTitle: this.state.title || recipe.title,
}
Full code can be found here (you can also test how it is currently working if it was hard to understand what I meant. Try to open any recipe, edit it and press button Edit Recipe and nothing will happen, recipe title will not be changed): https://codepen.io/andriusl/pen/LjxYQo
You directly accessed this.pendingTitle instead of this.state.pendingTitle. so change this to
handleClick(){
this.setState(
{title: this.state.pendingTitle, ingredients: this.state.pendingIngredients});
}
and change this code to
<a data-toggle="collapse" data-target={anchor_target}
href={anchor_target} className="collapsed">{this.state.title||recipe.title}
Related
Please, help me. I am doing PERN stack app, and I can't understand why my PUT method is not working in front end but works in Postman perfectly. When I add a todo - "drink water" for example, in database id appears as should "drink water" and in fron end as well it appears, but when I edit that todo in front end it's null in database and front end as well,
looks like that: description {id: 56, todo: null}
Here is a code of EditTodo.js:
export default function EditTodo({ todo }) {
const [description, setDescription] = useState(todo);
console.log("todos", todo);
console.log("description", description);
// Edit function
const editText = async (id) => {
try {
const body = { description };
const response = await fetch(`http://localhost:5000/todos/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
console.log("response", response);
window.location = "/";
} catch (error) {
console.error(error.message);
}
};
return (
<Fragment>
<button
type="button"
className="btn btn-warning"
data-toggle="modal"
data-target="#myModal"
>
Edit
</button>
<div className="modal" id="myModal">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">Edit Todo</h4>
<button
type="button"
className="close"
data-dismiss="modal"
></button>
</div>
<div className="modal-body">
<input
type="text"
className="form-control"
value={description.todo}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-warning"
data-dismiss="modal"
onClick={() => editText(todo.id)}
>
Edit
</button>
<button
type="button"
className="btn btn-danger"
data-dismiss="modal"
>
Close
</button>
</div>
</div>
</div>
</div>
</Fragment>
);
}
Looks like EditText function is not executed when I edit the todo.
Thanks in advance.
Where do you set the todo? A put request replaces the current values with new ones. If you only set the description property but leave the todo out, it will overwrite it with an empty string, clearing any data you had in there. Either set the old value of the todo before making your put request or use a patch instead. A patch only affect certain selected values instead of everything. Anyway, it's still very possible to do this with a put instead.
Try creating a useEffect where you set your todo to state.
Something like this:
const todoVar = description.find((desc)=> desc.id === todo.id);
useEffect(()=>{
if(todoVar){
setDescription(description.todo)
}
},[todoVar, setDescription])
Basically, you want to set the todo value from your database to it's original value before making your PUT request. This way it won't be empty/null.
So, I'm trying to create a button that when I click it, it should change into a input with a another button for submit. I'm using react and bootstrap.
<div className="cold-md-2 col-sm-3">
<div className="card-body">
<button class="btn btn-lg btn-block btn-group py-3" type="button">New group
<i class="fas fa-plus"></i></button>
</div>
</div>
You should probably create a state indicating wheter should render a button or an input, then on your render you check wich one you should render.
export default class Test extends PureComponent {
constructor(props) {
super(props);
this.state = {
type: 'button'
};
}
toggleType() {
this.setState({
type: this.state.type === 'button' ? 'input' : 'button'
});
}
render() {
if (this.state.type === 'input')
return <span>In here its the input HTML</span>;
return (
<button onClick={this.toggleType.bind(this)} type="button">Toggle</button>
);
}
}
To change the text of a button that has been declared with <input type="button"> tag: use the button's value property. For example:
<input type="button" value="Button Text" id="myButton">Submit</input>
i triying to find a solution for this particular problem but i can find it... in new with this, i have this code:
import React, { Fragment, useState } from 'react'
import '../../media/style/modal.css'
export default function Modal(props) {
const { activateModal } = props
const[values, setValues]= useState({name:'',email:''})
if (activateModal) {
document.getElementById('modals').click()
}
function handleValues(){
setValues({
name:document.getElementById('name').value,
email:document.getElementById('email').value
})
}
return (
<Fragment>
<button type="button" id="modals" style={{ display: 'none' }} data-toggle="modal" data-target="#exampleModalCenter">
Launch demo modal
</button>
<div className="modal fade" id="exampleModalCenter" tabIndex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
<div className="modal-dialog modal-dialog-centered" role="document">
<div className="modal-content">
<form onSubmit={handleSubmit}>
<div className="container">
<div className="row">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div className="row justify-content-center">
<h5 className="modal-title" id="modalTitle"><span style={{color:'rgb(197,115,199)'}}>get- </span>started</h5>
</div>
<div className="row justify-content-center">
<input type="text" id="name" name="name" onChange={handleValues} />
<input type="email" id="email" name="email" />
</div>
<div className="row justify-content-center">
<button type="submit" id="btn2">Send</button>
</div>
</div>
</form>
</div>
</div>
</div>
</Fragment>
)
}
When i write something in the input, my page re renders... and i dont know how to stop this behavior,any idea?... i try to use useRef but i dont know if im using it right so i dont get the desired effect
I copied your code to a sandbox and fixed it up for you https://codesandbox.io/s/agitated-snow-lls4g?fontsize=14
Just to clarify, useRef will not re-render, even if you mutate it. The updated value will only show when something else triggers a re-render. so useState was the correct usage.
Although I did not change this for you (in order to show that it is possible without it) I would highly recommend putting email and name in their own useState's so that changing the one does not change the other. I would also not get the values from document.getElementById but rather use event.target.value that is passed in from the handleChange, and give a handleChange for each element. If you got multiple elements, use functional programming to create their functions. (e.g. handleEmailChange = handleChange('email') and then handleChange is a function that returns a function (also known as currying).
To clarify:
useRef is to keep data between renders(updating does not fire re-rendering)
useState is to keep data between renders(updating will fire re-rendering)
each input should probably use its own state to avoid changing values that other components and effects might subscribe to.
use currying to compute a handleChange for each input
I am making a React page which has a post and comments on the post. Now the onChange on being triggered re-renders the whole class which makes the typing in input slow.
Now, if the is declared in a separate class and the value entered in the input there can be sent to the main class for API call. But I am not able to do this. Can anyone help me?
Below is code for my comment section of the screen.
commentChange(html) {
this.setState({ post_comment: html})
}
<div className="post-comments">
<div className="post-comments-head">
<div>Comments ({this.state.comments.length})</div>
</div>
<div className="comments">
{this.createCommentList(this.state.comments2)}
</div>
</div>
<div className="post-commenting">
{this.state.reply == -1 ? <span>Comment as {this.state.name}</span>
: this.commentBy()}
<div className="write-comment-post">
<ReactQuill
data-gramm_editor="false"
onChange={this.commentChange}
value={this.state.post_comment}
className="post_comments_x"
placeholder="Write a comment"
ref={(ip) => this.myInp = ip}
autoFocus={true}
theme=""
/>
<div className="comments-submit">
<button className="submit-comment"
onClick={() => this.submitComment(this.state.reply)}
disabled={!enabledComment}>
Comment
</button>
</div>
</div>
</div>
The createCommentList function takes comments and returns a nested list of comments. Below is the section where new comment is added.
How to solve this because it is making typing a new comment very slow.
<div className="write-comment-post">
<ReactQuill
data-gramm_editor="false"
onChange={this.commentChange}
value={this.state.post_comment}
className="post_comments_x"
placeholder="Write a comment"
ref={(ip) => this.myInp = ip}
autoFocus={true}
theme=""
/>
create a seperate component for this
and only onn enter call parennt function else onchange it will trigger
When I run this code, I get a bootstrap panel group for each recipe item inside of local storage. When I try to delete a recipe, sometimes the right recipe is removed and sometimes not. The console shows that the right recipe is removed from local storage but for some reason when the app component resets its state, the wrong recipe is removed. I've noticed that if I try to delete the recipes from the bottom up, it works. But if I click on the first recipe, the bottom recipe is removed.
I know this is an easy fix but I need a fresh perspective. Thanks everyone!
Also, sorry lack of indentation in the code - stack overflow wasn't being too friendly with the spacing
class App extends Component {
constructor() {
super();
this.deleteRecipe = this.deleteRecipe.bind(this)
this.state = {
recipeData: JSON.parse(localStorage.getItem('recipeData'))
}
}
deleteRecipe() {
this.setState({recipeData: JSON.parse(localStorage.getItem('recipeData'))})
}
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React Recipe Box!</h2>
</div>
<div className="container">
{this.state.recipeData.map(recipe => {
return (
<Recipe name={recipe.name} ingredients={recipe.ingredients} deleteRecipe={this.deleteRecipe}/>
)
})}
</div>
</div>
);
}
}
class Recipe extends Component {
constructor() {
super();
this.onDeleteRecipe = this.onDeleteRecipe.bind(this)
}
componentWillMount(){ //set state when component is about to mount
this.state = {
name: this.props.name,
ingredients: this.props.ingredients,
}
}
onDeleteRecipe() {
var recipeList = JSON.parse(localStorage.getItem('recipeData'));
for(var i = 0; i < recipeList.length; i++) {
if(recipeList[i].name === this.state.name) {
recipeList.splice(i, 1);
console.log("Deleted " + this.state.name, recipeList);
localStorage.removeItem('recipeData');
localStorage.setItem('recipeData', JSON.stringify(recipeList));
this.props.deleteRecipe();
}
}
}
render() {
return (
<div>
<div className="panel-group">
<div className="panel panel-primary">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-target={'#' + (this.state.name).replace(/\s/g, '')} href={'#' + (this.state.name).replace(/\s/g, '')}>
{this.state.name}
</a>
</h2>>
</div>
<div id={(this.state.name).replace(/\s/g,'')} className="panel-collapse collapse">
<div className="panel-body">
{this.state.ingredients.map(ingredient => {
return <li className="list-group-item">{ingredient}</li>
})}
<div className="btn-group">
<button className="btn btn-sm btn-info" data-toggle="modal"
data-target={'#' + (this.state.name).replace(/\s/g, '') + 'EditModal'}>Edit</button>
<button className="btn btn-sm btn-danger" data-toggle="modal"
data-target={'#' + (this.state.name).replace(/\s/g, '') + 'RemoveModal'}
>Delete</button>
</div>
</div>
</div>
</div>
</div>
<div className="modal modal-lg" id={(this.state.name).replace(/\s/g, '') + 'EditModal'} >
<div className="modal-content">
<div className="modal-header">
<h2>Edit {this.state.name}</h2>
</div>
<div className="modal-body">
<ul className="list-group list-unstyle">
{this.state.ingredients.map( ingredient => {
return <li className="list-group-item">{ingredient}</li>
})}
</ul>
</div>
<div className="modal-footer">
<div className="btn-group">
<button className="btn btn-sm btn-info" data-dismiss="modal">Save</button>
<button className="btn btn-sm btn-danger" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div className="modal modal-lg" id={this.state.name.replace(/\s/g, '') + 'RemoveModal'}>
<div className="modal-content">
<div className="modal-body">
<h3>This will remove the selected recipe. Are you sure?</h3>
</div>
<div className="modal-footer">
<div className="btn-group">
<button className="btn btn-sm btn-danger" data-dismiss="modal" onClick={this.onDeleteRecipe}>Delete</button>
<button className="btn btn-sm btn-info" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default App;
I'm still a novice, but... I built a react recipe box for FreeCodeCamp a few months ago, so I just pulled it up to compare my delete function to yours.
I notice that you treat localStorage differently than I did in mine. (Not that my way is best or even right!) I wonder if somehow the problem is rooted in that design. Your delete function goes into localStorage and makes changes, then you run a setState to sort of "re-get" the recipeData, if I'm reading right?
By contrast, my app declares a variable from localStorage and I set this.state.recipeArray to equal that variable. Then all of my edit/add/delete functions change this.state.recipeArray, not localStorage. Sorta like this:
handleDelete: function(e) {
e.preventDefault();
var replacementRecipeArray = this.state.recipeArray.filter((recipe) => recipe.title !== e.target.name);
//dot-filter with ES6 fat-arrow returns all items where title doesn't match the one we clicked; IOW, it removes the one we want to delete
this.setState({
recipeArray: replacementRecipeArray
});
}
In order to get any changes to this.state.recipeArray back to localStorage, I do a localStorage.setItem every time I render the page.
render() {
//first save current array to localstorage - wasn't reliable anywhere else
localStorage.setItem("_shoesandsocks_recipes", JSON.stringify(this.state.recipeArray));
//render page
return (
<div className="App">
<div className="recipeContainer">
// etc etc
For all I know this is a crazy design, but it works.