Ill preface this question with I am fairly junior, so my code might not adhere to best practices. That said, I have been creating a "to-do" list with full CRUD functionality. So far, I have been able to successfully populate the task but have gotten stuck with updating the task. I am successfully updating the task when I hardcode the index but haven't been successful in dynamically updating, as I can't seem to pass the index back up to the parent component.
Within my Home component, I have the following.... Within, "handleSubmit" function, is where I am updating the list.
HOME
class Home extends Component {
constructor(props) {
super(props);
this.state = {
allTodos: {
todo: '',
description: '',
urgency: '',
date: new Date(),
},
showList: false,
arrayOfTodos: [],
showModal: false,
index: 0,
};
}
handleChangeTodo = (e) => {
this.setState({
allTodos: {
...this.state.allTodos,
todo: e.target.value,
},
});
};
handleChangeDescription = (e) => {
this.setState({
allTodos: {
...this.state.allTodos,
description: e.target.value,
},
});
};
handleChangeUrgency = (e) => {
this.setState({
allTodos: {
...this.state.allTodos,
urgency: e.target.value,
},
});
};
handleChangeDate = (date) => {
this.setState({
allTodos: {
...this.state.allTodos,
date: date,
},
});
};
handleSubmit = (e) => {
e.preventDefault();
this.setState((prevState) => ({
arrayOfTodos: [...prevState.arrayOfTodos, prevState.allTodos],
allTodos: { todo: '', description: '', urgency: '', date: new Date() },
}));
this.setState({ showList: true });
if (this.state.showModal) {
this.state.arrayOfTodos.splice(0, 1);
this.setState({ showModal: false });
}
};
handleShowModal = () => {
this.setState({ showModal: true });
};
render() {
console.log(this.state.arrayOfTodos);
return (
<div>
<header> Task Buddy</header>
<div>
<form>
<label>
To-Do:
<input
type='text'
name='todo'
value={this.state.allTodos.todo}
onChange={this.handleChangeTodo}
/>
</label>
<label>
Description:
<input
type='text'
name='description'
onChange={this.handleChangeDescription}
value={this.state.allTodos.description}
/>
</label>
<label>
Urgency:
<input
type='text'
name='urgency'
onChange={this.handleChangeUrgency}
value={this.state.allTodos.urgency}
/>
</label>
<label>
Date:
<Calendar
// value={this.state.date}
onChange={this.handleChangeDate}
value={this.state.allTodos.date}
/>
</label>
<button onClick={this.handleSubmit}> Submit</button>
</form>
{this.state.showList && (
<div>
<Todos
index={this.state.index}
todo={this.state.arrayOfTodos}
showModal={this.state.showModal}
handleShowModal={this.handleShowModal}
/>
</div>
)}
{this.state.showModal && (
<div>
<Modal
todo={this.state.arrayOfTodos}
handleChangeTodo={this.handleChangeTodo}
handleChangeDescription={this.handleChangeDescription}
handleChangeUrgency={this.handleChangeUrgency}
handleChangeDate={this.handleChangeDate}
handleSubmit={this.handleSubmit}
/>
</div>
)}
</div>
</div>
);
}
}
export default Home;
TODOS
const Todos = (props) => {
console.log(props.index);
return (
<div>
{props.todo.map((x, index) => {
return (
<div>
<li key={index}>
{index}
{x.todo} {x.description} {x.urgency} {x.date.toString()}{' '}
<button onClick={props.handleShowModal}>Update</button>
<button> Delete</button>
</li>
</div>
);
})}
</div>
);
};
export default Todos;
MODAL
class Modal extends Component {
render(props) {
return (
<div>
<form>
<label>
To-Do:
<input
type='text'
name='todo'
value={this.props.todo.todo}
onChange={this.props.handleChangeTodo}
placeholder={this.props.todo.todo}
/>
</label>
<label>
Description:
<input
type='text'
name='description'
onChange={this.props.handleChangeDescription}
value={this.props.todo.description}
/>
</label>
<label>
Urgency:
<input
type='text'
name='urgency'
onChange={this.props.handleChangeUrgency}
value={this.props.todo.urgency}
/>
</label>
<label>
Date:
<Calendar
// value={this.state.date}
onChange={this.props.handleChangeDate}
value={this.props.todo.date}
/>
</label>
<button onClick={this.props.handleSubmit}> Submit</button>
</form>
</div>
);
}
}
export default Modal;
I'll give somewhat abstract answer with general practices that I often encounter and use.
Basically, there is one proper way to pass data from child component to parent component - via functions that passed from parent and called in child. If said data accessible in parent beforehand (e.g. during render) you can construct function with closure in such way that each component will call their own version of function with data already included. It will look somewhat like this:
render() {
return (
<>
{collection.map((item, index) => (
<Component
onEvent={(params) => {
/* You can use index and all other data accessible in parent together with params that were passed from component */
}}
/>
))}
</>
);
}
Another way is to construct handler in such way that you will pass in it all data that you need to pass. So, for example, if you tracking "change" event from input and can't rely on input name or id, you can pass additional data in your handler along with event like this:
// Parent.js
class Parent {
handleInputChange = (event, index) => {
/* do something with event index passed from child */
};
render() {
return (
<>
{collection.map((item, index) => (
<ComponentWithInput
key={index}
index={index}
onChange={this.handleChange}
/>
))}
</>
);
}
}
// Child.js
function ComponentWithInput(props) {
return <input onChange={(event) => props.onChange(event, props.index)} />;
}
Generally speaking, functions that you pass to child component is a way of communication in up direction, so you design these function not based solely on DOM events, but on data that you need to pass up. Sometimes you don't need DOM events at all, apart from the fact that they happened.
Related
I have a simple To do app that takes user input through a prompt and adds it to a list. I want to change the prompt and replace it with a text input bar. I've been having trouble implenting onChange and I'm hitting a wall trying to figure this out. I really appreciate the help as a beginner trying to learn.
import React, { useState } from 'react';
let id = 0
const Todo = props => (
<li>
<input type="checkbox" checked={props.todo.checked} onChange={props.onToggle} />
<button onClick = {props.onDelete}> delete</button>
<span>{props.todo.text}</span>
</li>
)
class App extends React.Component {
constructor() {
super()
this.state = {
todos: [],
}
}
addTodo() {
const text = prompt("TODO TEXT PLEASE!")
this.setState({
todos: [...this.state.todos, {id: id++, text: text, checked: false},
]
})
}
removeTodo(id) {
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id )
})
}
toggleTodo(id) {
this.setState({
todos: this.state.todos.map(todo => {
if (todo.id !== id) return todo
return {
id: todo.id,
text: todo.text,
checked: !todo.checked,
}
})
})
}
render() {
return (
<div>
<div> Todo Count: {this.state.todos.length}</div>
<div> Unchecked Count: {this.state.todos.filter(todo => !todo.checked).length} </div>
<div className="App">
<label> Task Name:</label>
<input type="text" id="task"
/* onChange={(e)=> {
setTaskName(e.target.value);*/
/>
<button onClick={() => this.addTodo()}> Add ToDo </button>
</div>
<ul>
{this.state.todos.map(todo => (
<Todo
onToggle={() => this.toggleTodo(todo.id)}
onDelete={() => this.removeTodo(todo.id)}
todo={todo}
/>
))}
</ul>
</div>
)
}
}
To resolve this issue you are having, you will need to also add a local state to store your input field data (you could get this with a Controlled or Uncontrolled approaches):
this.state = {
currentTodo: "",
todos: [],
}
And then in your input onChange event as you already did (you were using a hook based approach though), you could add a state update for your currentTodo value as you write. And also the current state value in the input tag (as Cesare observed):
<input type="text" id="task"
value={this.state.currentTodo}
onChange={(e)=> {
this.setState({ ...this.state, currentTodo: e.target.value});
}
/>
Finally, to obtain the wroten text you can get it in your addTodo method.
const text = this.state.currentTodo;
I need to access a method handleCancelEdit() defined in parent component. But, the matter here is that every child component will have its own cancelEdit state. Now, what is happening is, if I call handleCancelEdit() from one child component, every other of the same child components is taking the state and updating themselves(the method is not completely defined yet). That's why, I have defined the cancelEdit state in the child component, thinking that it belongs to this child component only.
Now, how do I make the handleCancelEdit() method make changes to the only child component which called it?
The parent:
function Parent() {
const handleCancelEdit = () => {
setCancelEdit(!cancelEdit); // defined in child component
setEdit(!edit); // defined in child component
...
};
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
on_change={(e)=>setEmail(e.target.value)}
on_click={handleUserEmail}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
on_change={(e)=>setAbout(e.target.value)}
on_click={handleUserAbout}
/>
</div>
);
}
Child component:
function ChildComponent({fieldName, value, inputType, placeHolder, name, on_change, on_click}) {
const [ edit, setEdit ] = useState(false);
const [ cancelEdit, setCancelEdit ] = useState(false)
const isEdit = edit;
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={e=>setEdit(!edit)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Save</button>
<button type="submit" onClick={handleCancelEdit}>Cancel</button>
</span>)
)}
</p>
);
};
I hope it could make it understandable that one child component should not make others to update. Now, how do I do it in this scenario?
EDIT
After making changes according to Linda Paiste:
The input field in the child component is not working even though the onChange in both parent and child is correct!
It is always more logical to pass state and data down rather than up. If the Parent needs to interact with the edit state then that state should live in the parent. Of course we want independent edit states for each child, so the parent can't just have one boolean. It needs a boolean for each child. I recommend an object keyed by the name property of the field.
In ChildComponent, we move isEdit and setEdit to props. handleCancelEdit is just () => setEdit(false) (does it also need to clear the state set by onChange?).
function ChildComponent({fieldName, value, inputType, placeHolder, name, onChange, onSubmit, isEdit, setEdit}) {
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={() =>setEdit(true)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Save</button>
<button type="submit" onClick={() => setEdit(false)}>Cancel</button>
</span>)
)}
</p>
);
};
In Parent, we need to store those isEdit states and also create a setEdit function for each field.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const handleSetEdit = (name, isEdit) => {
setIsEditFields((prev) => ({
...prev,
[name]: isEdit
}));
};
/* ... */
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
onChange={(e) => setEmail(e.target.value)}
onSubmit={handleUserEmail}
isEdit={isEditFields.email}
setEdit={(isEdit) => handleSetEdit("email", isEdit)}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
onChange={(e) => setAbout(e.target.value)}
onSubmit={handleUserAbout}
isEdit={isEditFields.about}
setEdit={(isEdit) => handleSetEdit("about", isEdit)}
/>
</div>
);
}
You can clean up a lot of repeated code by storing the values as a single state rather than individual useState hooks. Now 5 of the props can be generated automatically just from the name.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const [values, setValues] = useState({
about: '',
email: ''
});
const getProps = (name) => ({
name,
value: values[name],
onChange: (e) => setValues(prev => ({
...prev,
[name]: e.target.value
})),
isEdit: isEditFields[name],
setEdit: (isEdit) => setIsEditFields(prev => ({
...prev,
[name]: isEdit
}))
});
const handleUserEmail = console.log // placeholder
const handleUserAbout = console.log // placeholder
return (
<div>
<ChildComponent
fieldName={"Email"}
inputType={"text"}
placeHolder={"Enter email"}
onSubmit={handleUserEmail}
{...getProps("email")}
/>
<ChildComponent
fieldName={"About"}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
onSubmit={handleUserAbout}
{...getProps("about")}
/>
</div>
);
}
I've created a React component that takes inputs from other components to display text of various size in a view. Since it is basically a form, what I want to do is pass the current view into another page where I will then post that view to my database as JSON.
Since the state of the input fields are not set in this component, I'm not sure how I would pass them as props to a new view.
This is a condensed version of what my data input component looks like:
INPUTSHOW.JSX
export default class InputShow extends Component {
componentDidMount() {
autosize(this.textarea);
}
render() {
const { node } = this.props;
...
return (
<div className="editor-div" >
{
(node.type === 'buttonA') ?
<textarea
style={hlArea}
ref={a => (this.textarea = a)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-hl' type="text" onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<textarea
style={subArea}
ref={b => (this.textarea = b)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-sub' type="text" onChange={this.props.inputContentHandler} />
:
""
}
</div >
)
}
}
This works fine in creating inputs in a current view. I then pass those values to TextAreaField.JSX
export default (props) => {
return (
<>
<button><Link to={{
pathname: '/edit/preview',
text: props.inputsArray
}}>preview</Link></button>
<div className='view'>
{
props.inputsArray.map(
(node, key) => <InputShow key={key} node={node} inputContentHandler={props.inputContentHandler} />
)
}
</div>
</>
)
}
and then finally that is rendered in my Edit.JSX form:
export default class Edit extends React.Component {
constructor(props) {
super(props)
UniqueID.enableUniqueIds(this);
this.state = {
inputs: [],
text: ''
}
}
...
createPage = async () => {
await this.props.postPage(this.state.text)
}
// Handler for listen from button.
buttonCheck = (e) => {
index++;
const node = {
id: this.nextUniqueId() + index,
type: e.target.id,
text: '',
image: true
}
this.setState(
prev => ({
inputs: [...prev.inputs, node]
})
)
console.log(this.state.inputs);
}
inputContentHandler = (e) => {
let newArray = this.state.inputs;
let newNode = newArray.find((node) => {
return (node.id === e.target.id)
})
newNode.text = e.target.value;
this.setState({ inputs: newArray });
console.log(this.state.inputs);
}
render() {
return (
<div>
<InnerHeader />
<div className='some-page-wrapper'>
<div className='row'>
<div className="dash-card-sm">
<br />
<EditButtonContainer buttonCheck={this.buttonCheck} />
<Route path='/edit/form' render={() => (
<TextAreaField
inputsArray={this.state.inputs}
inputContentHandler={this.inputContentHandler}
/>
)}
/>
<Route path='/edit/preview' render={(props) => (
<Preview
inputs={this.state.inputs}
text={this.state.text}
createPage={this.createPage}
/>
)}
/>
<br /> <br />
{/* Button Header */}
</div>
</div>
</div>
</div>
)
}
}
The problem is that I don't know how I should be passing the rendered view to the Preview.jsxcomponent. I'm still new to react (4 months)...Any help in pointing me in the right direction would be appreciated.
I try to pass state to the child, to update the list of objects.
When I add an entry, it's not rendered in the child component.
I also checked that state.contacts actually gets replaced with new array, but it didn't work.
constructor(props) {
this.super(props);
}
removeContact(event) {
this.setState((state) => {
state.contacts = state.contacts.filter((contact) => contact.key !== event.target.key )
return state;
})
}
render() {
return (
<Fragment>
<span>{this.props.contact.name}</span>
<span>{this.props.contact.phone}</span>
<span>{this.props.contact.adress}</span>
<a href="#" onClick={this.removeContact}>X</a>
</Fragment>
)
}
}
class Contacts extends Component {
constructor(props) {
super(props);
this.state = { contacts: props.contacts };
}
render() {
console.log(this.state.contacts); // always displays empty array
return (
<div>
{this.state.contacts.map((contact, index) =>
<div>
<Contact key={index} contact={contact} contacts={this.state.contacts}/>
</div>
)}
</div>
)
}
}
class App extends Component {
state = {
time: new Date(),
name: "",
phone: "",
adress: "",
contacts: []
}
change = (event) => {
let nameOfField = event.target.name;
this.setState({[nameOfField]: event.target.value})
}
// click = () => {
// this.setState((state) => {
// state.time = new Date();
// return state;
// })
// }
addContact = () => {
let name = this.state.name;
let phone = this.state.phone;
let adress = this.state.adress;
this.setState((state) => {
return {contacts: [ ... state.contacts.concat([{name, adress, phone}])]}
});
}
render() {
return (
<div className="App">
<Timestamp time={this.state.time}/>
<Contacts contacts={this.state.contacts}/>
<input name="name" value={this.state.name} onChange={this.change} placeholder="Name"/>
<input name="phone" value={this.state.phone} onChange={this.change} placeholder="Phone"/>
<input name="adress" value={this.state.adress} onChange={this.change} placeholder="Adress"/>
<button onClick={this.addContact}>Add contact</button>
</div>
)
}
}
ReactDOM.render(<App time={Date.now().toString()}/>, document.getElementById('root'));
If values are passed to Components you should render them as props. There is no need to copy into the child component state:
class Contacts extends Component {
render() {
console.log(this.props.contacts); // use props instead of state
return (
<div>
{this.props.contacts.map((contact, index) =>
<div>
<Contact key={index} contact={contact} contacts={this.props.contacts}/>
</div>
)}
</div>
)
}
}
Using this.props is good practice because it allows React to deterministically render (If the same props are passed, the same render result is returned).
You are currently modifying the state in Contacts from it's child component Contact. You can't update a parents state directly from within a child component.
What you could do is create a removeContact function in your Contacts component and pass the entire function down to your Contact component. That way when you call removeContact in your child component, it will actually call it from the parent, modify the parents state, and update all it's children with the new state.
I am making a dashboard component which displays rendered previews and code for HTML snippets. Inside of the dashboard component I am mapping the array of snippets using .map. Each mapped snippet is going to have a delete function (already built) and an update function.
For the update function to work each snippet has it's own child modal component. I need to pass the ID of the snippet to the modal component where I can combine the ID with the new content before updating the database and state.
However, I'm making a mistake somewhere as I pass the ID as props to the modal.
.map used inside of my Dashboard.js Dashboard class component.
{this.state.snippets.map(snippet => (
<>
<div key={snippet._id} className="holder--pod">
<div className="content">
<div className="content__snippet-preview">
Snippet preview
</div>
<div className="content__body">
<h4>{snippet.name}</h4>
<p>{snippet.details}</p>
<p>{snippet._id}</p> //THIS WORKS
<pre>
<code>{snippet.content}</code>
</pre>
</div>
<div className="content__button">
<button onClick={this.handleDelete(snippet._id)}>
Delete
</button>
<button type="button" onClick={this.showModal}>
Open
</button>
</div>
</div>
</div>
<Modal
sid={snippet._id} //PASS ID HERE
show={this.state.show}
handleClose={this.hideModal}
></Modal>
</>
))}
This renders the snippets below (3 snippet pods, with their database ID included).
The open button opens the modal (Modal.js) below.
import React, { Component } from 'react'
import api from '../api'
export default class Modal extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
details: '',
content: '',
message: null,
}
}
handleInputChange = event => {
this.setState({
[event.target.name]: event.target.value,
})
}
handleClick = id => event => {
event.preventDefault()
console.log(id)
}
render() {
const { sid, show, handleClose } = this.props
console.log(sid)
const showHideClassName = show ? 'modal display-flex' : 'modal display-none'
return (
<div id="Modal" className={showHideClassName}>
<div id="modal-main">
<h4>Edit snippet {sid}</h4>
<form>
Name:{' '}
<input
type="text"
value={this.state.name}
name="name"
onChange={this.handleInputChange}
/>{' '}
<br />
Details:{' '}
<input
type="text"
value={this.state.details}
name="details"
onChange={this.handleInputChange}
/>{' '}
<br />
Content:{' '}
<textarea
value={this.state.content}
name="content"
cols="30"
rows="10"
onChange={this.handleInputChange}
/>{' '}
<br />
<button onClick={this.handleClick(sid)}>TEST ME</button>
</form>
<button onClick={handleClose}>Close</button>
{this.state.message && (
<div className="info">{this.state.message}</div>
)}
</div>
</div>
)
}
}
The console.log just under the render actually pastes the correct 3 ID's the console.
However, calling the ID (sid) within the Modal.js return will only show the last snippet ID, no matter which Modal I open. The same goes for pushing that ID to the handleClick function where I intend to combine the ID with an update package.
Solution below as initiated by HMR in the comments.
The problem was all the modals were showing and just the last one was visible.
Fixed by moving the modal out of the .map and instead updating the ID from within the .map to the state and passing the state ID to a new nested component within the modal.
Also switched to using dynamic CSS to show and hide the modal based on the state.
Dashboard.jsx
export default class Snippets extends Component {
constructor(props) {
super(props)
this.showModal = React.createRef()
this.state = {
snippets: [],
show: false,
sid: '',
}
}
handleDelete = id => event => {
event.preventDefault()
api
.deleteSnippet(id)
.then(result => {
console.log('DATA DELETED')
api.getSnippets().then(result => {
this.setState({ snippets: result })
console.log('CLIENT UPDATED')
})
})
.catch(err => this.setState({ message: err.toString() }))
}
handleModal = id => {
this.setState({ sid: id })
this.showModal.current.showModal()
}
//<div id="preview">{ReactHtmlParser(snippet.content)}</div>
render() {
return (
<>
<Modal ref={this.showModal} handleClose={this.hideModal}>
<ModalUpdate sid={this.state.sid} />
</Modal>
<div className="Dashboard">
<div className="wrapper">
<div className="container">
<div className="holder">
<div className="content">
<div className="content__body">
<h3>Dashboard</h3>
</div>
</div>
</div>
<div className="break"></div>
{this.state.snippets.map(snippet => (
<div key={snippet._id} className="holder--pod">
<div className="content">
<div className="content__snippet-preview">
Snippet preview
</div>
<div className="content__body">
<h4>{snippet.name}</h4>
<p>{snippet.details}</p>
<p>{snippet._id}</p>
<pre>
<code>{snippet.content}</code>
</pre>
</div>
<div className="content__button">
<button onClick={this.handleDelete(snippet._id)}>
Delete
</button>
<button
type="button"
onClick={() => this.handleModal(snippet._id)}
>
Open
</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</>
)
}
Modal.jsx
import React, { Component } from 'react'
export default class Modal extends Component {
constructor(props) {
super(props)
this.state = {
show: false,
}
}
showModal = () => {
this.setState({ show: true })
}
hideModal = () => {
this.setState({ show: false })
}
render() {
return (
<div
id="Modal"
style={{ display: this.state.show === true ? 'flex' : 'none' }}
>
<div id="modal-main">
<h4>Edit snippet </h4>
{this.props.children}
<button onClick={() => this.hideModal()}>Close</button>
</div>
</div>
)
}
}
ModalUpdate.jsx
import React, { Component } from 'react'
export default class ModalUpdate extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
details: '',
content: '',
message: null,
}
}
// handleInputChange = event => {
// this.setState({
// [event.target.name]: event.target.value,
// })
// }
// handleClick = id => event => {
// event.preventDefault()
// console.log(id)
// }
render() {
return <h4>ID = {this.props.sid}</h4>
}
}
I am not sure about the handleDelete function,. but replacing the line should solve the issue probably
<button onClick={() => this.handleDelete(snippet._id)}>
One potential issue is the this.handleDelete(snippet._id) will fire immediately rather than onClick, so you will need to add an anonymous function in the event listener:
() => this.handleDelete(snippet._id)
instead of
this.handleDelete(snippet._id)