Just learning React, and I would like to add an onClick on the font awesome icon, and run the markTaskAsCompleted function. I'm having trouble because it's several components lower in the hierarchy. How would you ideally go about this? Bear in mind that I also have to pass the ID of the task in the function.
class TasksBase extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
tasks: [],
};
}
componentDidMount() {
this.onListenForTasks();
}
onListenForTasks() {
this.setState({ loading: true });
this.unsubscribe = this.props.firebase
.tasks()
.orderBy('created', 'desc')
.onSnapshot(snapshot => {
if (snapshot.size) {
let tasks = [];
snapshot.forEach(doc =>
tasks.push({ ...doc.data(), uid: doc.id }),
);
this.setState({
tasks: tasks,
loading: false
});
} else {
this.setState({ tasks: null, loading: false });
}
});
}
markTaskAsCompleted(){
console.log("Completed");
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { tasks, loading } = this.state;
return (
<div>
{loading && <div>Loading ...</div>}
{tasks ? (
<TaskList tasks={tasks} />
):(
<div>There are no tasks ...</div>
)}
</div>
);
}
}
const Tasks = withFirebase(TasksBase);
const TaskList = ({ tasks }) => (
<ul className="tasks">
{tasks.map( task => (
<Task key={task.uid} task={task} />
))}
</ul>
);
const Task = ({ task }) => (
(!task.completed && !task.obsolete && !task.waitingForDependencies) &&
<li className="task">
<strong>{task.userId}</strong> {task.name}
<div className="icons">
<FontAwesomeIcon icon="check-circle"/>
<FontAwesomeIcon icon="times-circle" />
</div>
</li>
);
const condition = authUser => !!authUser;
export default compose(
withEmailVerification,
withAuthorization(condition),
)(Tasks);
Bind your class function in the constructor:
this.markTaskAsCompleted = this.markTaskAsCompleted.bind(this);
Pass the function into the child component with props:
<TaskList tasks={tasks} handleMarkCompleted={this.markTaskAsCompleted} />
Pass the function again to child component, this is prop drilling and is not the latest greatest approach but it works:
const TaskList = ({ tasks, handleMarkCompleted }) => (
<ul className="tasks">
{tasks.map( task => (
<Task key={task.uid} task={task} handleMarkCompleted={handleMarkCompleted} />
))}
</ul>
);
Trigger the function with onClick:
inside <Task>...
<FontAwesomeIcon icon="check-circle" onClick={() => handleMarkCompleted(task.uid)} />
If passing data into the function (ex. task.uid) make it a param in the function definition as well so you can use it:
markTaskAsCompleted(id){
console.log("Completed", id);
}
you will need to pass it down the tree as
markTaskAsCompleted={this.props.markTaskAsCompleted}
and make sure that the function is bound to the parent in the constructor.
u can use refs or document.getElementById to get the ID.
Related
Let's say I have this component:
class MyForm {
render() {
return (
<>
<h3>Remember my Setting</h3>
<Toggle
value={this.state.rememberMe}
onClick={() => {
this.setState(prevState => ({ rememberMe: !prevState.rememberMe }));
}}
/>
</>
)
}
}
How can I pass this prevState if its wrapped in child component? Is this acceptable?
class MyForm {
render() {
return (
<PartialForm
onClick={(v) => {
this.setState({ rememberMe: v });
}}
/>
)
}
}
const PartialForm = (props) => {
return (
<>
<h3>Remember my Setting</h3>
<Toggle
value={props.rememberMe}
onClick={() => {
props.onClick(!props.rememberMe);
}}
/>
</>
);
}
I want to know if accessing props.rememberMe is the same and safe as accessing prevState.rememberMe in parent component. Thanks
In my opinion, the way you're doing is safe and it has no conflicts at all.
I can simply demonstrate those re-rendering steps here:
Pass the current state to PartialForm
Trigger toggle click props.onClick for updating rememberMe in the upper component
The upper component gets re-rendered, and then your child component will have the latest state of rememberMe
By the way, you also forget to pass rememberMe in <PartialForm />
class MyForm {
render() {
return (
<PartialForm
rememberMe={rememberMe}
onClick={(v) => {
this.setState({ rememberMe: v });
}}
/>
)
}
}
const PartialForm = (props) => {
return (
<>
<h3>Remember my Setting</h3>
<Toggle
value={props.rememberMe}
onClick={() => {
props.onClick(!props.rememberMe);
}}
/>
</>
);
}
Once the user updates the input inbox displayed eName should change. It is not updating the array. What is causing this?
Error message:
TypeError: Cannot set property 'eName' of undefined App.inputChangeHandler
Code:
class App extends Component {
state= {
product:[
{eName:"Anu", eNo:"1200", eSalary:"1000"},
{eName:"Jack", eNo: "1201", eSalary:"1200"},
{eName:"Ben", eNo: "1202", eSalary:"1300"}
],
showFlag:true,
}
inputChangeHandler(event,index){
const mProducts = this.state.product;
mProducts[index].eName = event.target.value;
console.log(event.target.value);
this.setState({
product:mProducts
})
}
deleteHandler(index){
console.log("delete clicked" + index);
const mProducts = this.state.product;
mProducts.splice(index,1);
this.setState({
product:mProducts
})
}
showHideHandler=()=>{
this.setState({
showFlag:!this.state.showFlag
})
console.log(this.state.showFlag);
}
render(){
let dp = null;
if (this.state.showFlag === true){
dp =(
<div className ="App">
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key ={index}
click ={this.deleteHandler.bind(this.index)}
inputChange={this.inputChangeHandler.bind(this)}
/>)
})}
</div>
)
}
return(
<div className ="App">
{dp}
<hr/>
<button onClick={()=>this.showHideHandler()}>show hide</button>
<h2>{this.state.eName} </h2>
</div>
);
}
}
export default App;
Once the user updates the input inbox displayed eName should change. It is not updating the array.
The issue is because the inputChangeHandler function is not receiving the correct index. Please make the below changes:
define inputChangeHandler function as arrow function:
inputChangeHandler = (event,index) => {
const mProducts = this.state.product;
mProducts[index].eName = event.target.value;
console.log(event.target.value);
this.setState({
product:mProducts
})
}
Inside render function, while calling inputChangeHandler, pass both event and index
render(){
let dp = null;
if (this.state.showFlag === true){
dp =(
<div className ="App">
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key ={index}
click ={this.deleteHandler.bind(this.index)}
inputChange={(e) => this.inputChangeHandler(e, index)}
/>)
})}
</div>
)
}
return(
<div className ="App">
{dp}
<hr/>
<button onClick={()=>this.showHideHandler()}>show hide</button>
<h2>{this.state.eName} </h2>
</div>
);
}
You are mutating state and do a setState with the mutated state in all your handlers. Because React does not see a change in state it won't re render (try to put a console.log in render and you'll see it won't show in console).
Here is how you set a value in state without mutating it:
inputChangeHandler(event, index) {
this.setState({
product: this.state.product.map((product, i) =>
i === index
? { ...product, eName: event.target.value }
: product
),
});
}
I would advice better naming for your variable, if you have a list of products it would be better to call it products (plural) instead of product (single).
If you have React render a list of elements and you can delete or sort elements then you should not use index as key, doesn't product have a unique id you can use and if not then why not?
Below is an example of how you could send event handlers to Product:
class App extends React.Component {
state = {
products: [
{ id: 1, eName: 'Anu', eNo: '1200', eSalary: '1000' },
{
id: 2,
eName: 'Jack',
eNo: '1201',
eSalary: '1200',
},
{ id: 3, eName: 'Ben', eNo: '1202', eSalary: '1300' },
],
showFlag: true,
};
//arrow function will auto bind
inputChangeHandler = (eName, id) => {
this.setState({
products: this.state.products.map(p =>
p.id === id ? { ...p, eName } : p
),
});
};
//arrow function will auto bind
deleteHandler = id => {
this.setState({
products: this.state.products.filter(
p => p.id !== id
),
});
};
showHideHandler = () => {
this.setState({
showFlag: !this.state.showFlag,
});
};
render() {
let dp = null;
if (this.state.showFlag === true) {
dp = (
<div className="App">
{this.state.products.map((product, index) => {
return (
<Product
key={product.id}
{...product}
remove={this.deleteHandler}
inputChange={this.inputChangeHandler}
/>
);
})}
</div>
);
}
return (
<div className="App">
{dp}
<hr />
<button onClick={() => this.showHideHandler()}>
show hide
</button>
<h2>{this.state.eName} </h2>
</div>
);
}
}
//make this a pure component with React.memo
const Product = React.memo(function Product({
remove,
id,
eName,
inputChange,
}) {
console.log('rendering:', id);
return (
<div>
<button onClick={() => remove(id)}>delete</button>
<input
type="text"
value={eName}
onChange={e => inputChange(e.target.value, id)}
></input>
</div>
);
});
//render app
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You should also pass the index to your Product component,
{this.state.product.map((product,index) => {
return (
<Product
eName={product.eName}
eNo={product.eNo}
eSalary={product.eSalary}
key = {index}
ind = {index} //pass index here
click ={this.deleteHandler.bind(this.index)}
inputChange={this.inputChangeHandler.bind(this)}
/>
)
})}
your Product component should be this (as per your comment),
import React from 'react'
import './employee.css'
function Employee(props) {
return (
<div className ="prod">
<h1> Name:{props.eName}</h1>
<h2> Emp-No:{props.eNo}</h2>
<h3> Salary:{props.eSalary}</h3>
<button onClick ={props.click}> delete</button>
<input onChange={(e) => props.inputChange(e,props.ind)} value={props.eName} type="text"/> //pass `event` & `index` to `inputChange` function here
</div>
)
}
export default Employee;
I have a dynamic todo list I would like to add a "highlight" feature to. Each list item renders with markup for the highlight that should show only for the list item clicked.
export class Todo extends Component {
constructor(props) {
super(props);
this.state = {input: '', todos: this.getOldTodo()};
this.selectItem = this.selectItem.bind(this);
}
//shortened
selectItem(i) {
this.setState({selected: i});
if (this.state.selected == i) {
// --- this is the code that needs to change the right list items child's class
???.props.childen[2].className = "active";
// ---
console.log("true")
}
console.log(i);
}
render() {
//markup also shortened
this.state.todos.map((todos, i) => {
return (
//What do I pass to the method here?
<li key={todos.key} className="todo-li-item" onClick={this.selectItem.bind(this, i)}>
<span className="todo-item">{todos.text}</span>
<span onClick={this.deleteItem.bind(this, i)} className="delet-todo">✕</span>
// --- This is the child that needs its class changed when it's parent is clicked
<div id="todo-select" className={"hidden"}>
<span id="todo-select-top"></span>
<span id="todo-select-left"></span>
</div>
</li>
);
})
</ul>
</div>
);
}
}
This is painfully simple and yet so un-obvious as to what I use to do this in react, but hey I'm still learning. Thanks for your time.
You've been quite close. Here's my implementation.
Key takeaway: Don't mutate the state object.
selectItem(idx) {
this.setState(state => {
const todos = [
state.todos.slice(0, idx),
{ ...state.todos[idx], selected: ! state.todos[idx].selected },
state.todos.slice(idx + 1, state.todos.length),
]
return {
...state,
todos,
}
})
}
deleteItem(idx) {
this.setState(state => {
const todos = [...state.todos]
todos.splice(idx, 1)
return {
...state,
todos,
}
})
}
render() {
return (
<div>
<ul>
{this.state.todos.map((todo, idx) => (
<li
key={todo.key}
className={'todo-li-item'}
onClick={this.selectItem.bind(this, idx)}
>
<span className="todo-item">{todo.text}</span>
<span
onClick={this.deleteItem.bind(this, idx)}
className="delete-todo"
>
✕
</span>
<div id="todo-select" className={todo.selected && 'active'}>
<span id="todo-select-top" />
<span id="todo-select-left" />
</div>
</li>
))}
</ul>
</div>
)
}
The list item can be a stateless component, so the onSelect and onDelete become callback functions.
Deleting item with index may get you in trouble, since React will not re-render the entire list every time.
I don't know what's inside getOldTodo, but custructor cannot wait. So it will be null initially, if it's an async function.
There is an implementation using ES6 syntax.
Each list item is stateless:
const ListItem = props => {
const { todo, deleteItem, selectItem } = props;
return (
<li key={todo.key} className="todo-li-item" onClick={selectItem}>
<span className="todo-item">{todos.text}</span>
<span onClick={deleteItem} className="delet-todo">
✕
</span>
clicked
<div id="todo-select" className={'hidden'}>
<span id="todo-select-top" />
<span id="todo-select-left" />
</div>
</li>
);
};
All events are handled by a stateful component:
export class Todo extends Component {
state = {
input: '',
todos: [],
};
async componentDidMount() {
const todos = await this.getOldTodo();
this.setState({ todos });
}
render() {
return (
<div>
{this.state.todos.map(todo => (
<ListItem
todo={todo}
key={todo.key}
selectItem={() => {
this.selectItem(todo);
}}
deleteItem={() => {
this.deleteItem(todo);
}}
/>
))}
</div>
);
}
selectItem = todo => {
const idx = this.state.todos.findIndex(i => i.key === todo.key);
const todos = this.state.todos.slice();
const todo = { ...this.state.todos[idx] };
// change
todos[idx] = todo;
this.setState({
todos
});
}
deleteItem = todo => {
const idx = this.state.todos.findIndex(i => i.key === todo.key);
const todos = this.state.todos.splice(idx, 1);
this.setState({
todos
});
}
getOldTodo = async () => {
//...
}
}
Does this make sense to you?
Help! My child component is not updating in my react app!
I want to bring cartNumber to the page component which then is passed onto header component but the number doesn't even show up!
Parent component
class Shop extends Component {
constructor(props) {
super(props);
this.state = {
merchants: [],
error: null,
loading: true,
order: []
};
}
componentWillMount() {
Meteor.call("merchants.getMerchants", (error, response) => {
if (error) {
this.setState(() => ({ error: error }));
} else {
this.setState(() => ({ merchants: response }));
}
});
}
componentDidMount() {
setTimeout(() => this.setState({ loading: false }), 800); // simulates loading of data
}
goBack = () => this.props.history.push("/");
goCart = () => {
try {
Orders.insert(this.state.order), this.props.history.push("/cart");
} catch (error) {
throw new Meteor.Error("there was an error", error);
}
};
onAddToCart(cartItem) {
let { order } = this.state;
order.push(cartItem);
console.log(order.length);
}
render() {
const { loading } = this.state;
const { merchants, error } = this.state;
const { data } = this.state;
const { order } = this.state;
const getProductsFromMerchant = ({ products, brands }) =>
products.map(({ belongsToBrand, ...product }) => ({
...product,
brand: brands[belongsToBrand]
}));
const products = merchants.reduce(
(acc, merchant) => [...acc, ...getProductsFromMerchant(merchant)],
[]
);
if (loading) {
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
>
<div className="loading-page">
<i
className="fa fa-spinner fa-spin fa-3x fa-fw"
aria-hidden="true"
/>
<br /> <br />
<span>Loading...</span>
</div>
</Page>
);
}
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
cartNumber={order.length}
>
<div className="shop-page">
{products.map(({ id, ...product }) =>
<Product
{...product}
key={id}
history
onAddToCart={this.onAddToCart.bind(this)}
/>
)}
</div>
</Page>
);
}
}
export default Shop;
Here is the page component which contains the header component
export const Page = ({
children,
pageTitle,
history,
goBack,
goCart,
cartNumber
}) =>
<div className="page">
<Header goBack={goBack} goCart={goCart} history cartNumber>
{pageTitle}
</Header>
<main>
<MuiThemeProvider>
{children}
</MuiThemeProvider>
</main>
<Footer />
</div>;
export default Page;
And Finally this is the header where I want to bring the cartNumber into.
const Header = ({ children, goBack, goCart, cartNumber, pageTitle }) =>
<header>
<button onClick={goBack} className="back-button">
{/* Image added here to show image inclusion, prefer inline-SVG. */}
<img alt="Back" src={`/icon/header/back-white.svg`} />
</button>
<h1>
{children}
</h1>
<div className="right-content">
( {cartNumber} )
<i
className="fa fa-shopping-cart fa-2x"
aria-hidden="true"
onClick={goCart}
/>
</div>
</header>;
export default withRouter(Header);
You're passing cartNumber as a boolean:
<Header goBack={goBack} goCart={goCart} history cartNumber>
Pass it as a value:
<Header goBack={goBack} goCart={goCart} history={history} cartNumber={cartNumber}>
I am trying to add a line-through on after checking a checkbox. I'm using react and redux. The action and reducer works. I just need a way of adding this line-through when checked is true Please find the code i tried implementing this below. Thanks in advance.
/actions/items.js
export const CHECK_ITEM = "CHECK_ITEM"
export function checkItem(id) {
return {
type: CHECK_ITEM,
id
}
}
/reducers/items.js
case types.CHECK_ITEM:
return state.map((item) => {
if(item.id === action.id) {
return Object.assign({}, item,
{
checked: !item.checked
})
}
return item
})
/components/Editor.jsx
renderValue = () => {
const onDelete = this.props.onDelete
const onCheck = this.props.onCheck
return (
<div>
{onCheck ? this.renderCheckItem() : null}
<div onClick={this.props.onValueClick}>
<span className='value'>{this.props.value}</span>
{onDelete ? this.renderDelete() : null}
</div>
</div>
)
}
renderCheckItem = () => {
return (
<input
type="checkbox"
className='check-item checked'
defaultChecked={false}
onClick={this.props.onCheck}
/>
)
}
/components/Item.jsx
export default class Items extends React.Component {
render () {
const {items, onEdit, onDelete, onValueClick, onCheck, isEditing} = this.props
return (
<ul className="items">{items.map(item =>
<Item
className="item"
key={item.id}
id={item.id}>
<Editor
value={item.text}
onCheck={onCheck.bind(null, item.id)}
style={{textDecoration: item.checked ? 'line-through' : 'none'}}
/>
</Item>
)}</ul>
)
}
}
You need to connect your components to the redux store. Here's how to do it. In short you need something like:
export default connect(
state => {
return {items: state.items};
}
)(Items);
Where connect comes from react-redux.
I basically passed item.checked as item to my Editor component and used it like so
...
render() {
const {item, value, onEdit, onValueClick, isEditing, onCheck, ...props} = this.props
...
then in my Editor.jsx i did the following
/components/Editor.jsx
renderValue = () => {
const onDelete = this.props.onDelete
const onCheck = this.props.onCheck
const itemChecked = this.props.item
const isChecked = {textDecoration: itemChecked ? 'line-through' : 'none'}
return (
<div>
{onCheck ? this.renderCheckItem() : null}
<div onClick={this.props.onValueClick}>
<span style={isChecked} className='value'>{this.props.value}</span>
{onDelete && this.renderDelete()}
</div>
</div>
)
}
renderCheckItem = () => {
return (
<input
type="checkbox"
className='check-item'
defaultChecked={false}
onClick={this.props.onCheck}
/>
)
}
/components/Items.jsx
export default class Items extends React.Component {
render () {
...
return (
<ul className='items'>{items.map((item) =>
<Item
className='item'
key={item.id}
id={item.id}>
<Editor
item={item.checked}
isEditing={item.isEditing}
...