I'm learning react at the moment and currently, making a todo app so that I can understand react more easily.
So here's what I'm trying to do:
The user clicks a button
The click fires a prompt which asks the user for the todo title (only title at the moment)
Then, that title is added to an array of all todos
And then, use that array to display each todo on the page
Code:
const [check, setCheck] = useState(false);
const [todo, setTodo] = useState([]);
function handleClick() {
let toAdd = prompt('Title: ')
setTodo([...todo, {
title: toAdd
}]);
}
useEffect(()=> {
if(todo.length !== 0) {
setCheck(true);
}
})
return (
<div className="wholeContainer">
<div className="tododiv">
<span className="todos">Todos: </span>
<hr/>
{
check ?
todo.forEach((eachTodo)=> {
<TodoItems title={eachTodo}/>
})
: <span>Nothing</span>
}
</div>
<button className="add" onClick={handleClick}>
<i className="fas fa-plus"></i>
Add a Todo
</button>
</div>
);
The const [check, setCheck] = useState(false); is written so that I can access the array if todo.length !== 0;
The problem comes in the rendering part. I can't figure out a way to display each and every todo in their own <TodoItems/> component, and also when using forEach(), nothing happens because I think that someone told me that setState() works asynchronously.
I really need some advice!
Thanks...
You are using
todo.forEach((eachTodo)=> {
<TodoItems title={eachTodo}/>
})
When you should be using
todo.map((eachTodo)=> {
return <TodoItems title={eachTodo}/>
})
Or
todo.map((eachTodo)=> (
<TodoItems title={eachTodo}/>
))
Also you have an infinite loop in your component:
useEffect(()=> {
if(todo.length !== 0) {
setCheck(true);
}
})
Each time the component updates, when the todo list isn't empty, you setCheck to true which triggers a new render.
Also, you don't need to use state for every variable, only the ones were a change should trigger a re-render.
Also your new todo-list state depends on the previous state so you should use a functional update.
https://reactjs.org/docs/hooks-reference.html
const [todoList, setTodoList] = useState([]);
function handleClick() {
let toAdd = prompt('Title: ');
setTodoList((prevTodoList) => [...prevTodoList, toAdd]);
}
const isTodoListEmpty = todoList.length === 0
return (
<div className="wholeContainer">
<div className="tododiv">
<span className="todos">Todos: </span>
<hr />
{!isTodoListEmpty ? (
todoList.forEach((todoItem) => {
<TodoItems title={todoItem} />;
})
) : (
<span>Nothing</span>
)}
</div>
<button className="add" onClick={handleClick}>
<i className="fas fa-plus"></i>
Add a Todo
</button>
</div>
);
Related
i'm building a to-do app using React Js . inside the task component i used a state to apply a certain styles for the completed task and it works fine . but , after i cliked any delete button the style of the completed task deleted . how can i prevent that ?
import Task from "../Task/Task";
import style from "./TasksList.module.css";
const TasksList = ({ tasks, deleteTaskHandler }) => {
return (
<div className={style.tasks}>
<div className="container">
{tasks.map((task, idx) => {
return (
<Task
task={task}
id={idx}
key={Math.random()}
deleteTaskHandler={deleteTaskHandler}
/>
);
})}
</div>
</div>
);
};
export default TasksList;
import { useState } from "react";
import style from "./Task.module.css";
const Task = ({ task, id, deleteTaskHandler }) => {
const [isComplete, setIsComplete] = useState(false);
const markComplete = () => {
setIsComplete(!isComplete);
};
return (
<div
className={
isComplete ? `${style.task} ${style.completed}` : `${style.task}`
}
onClick={markComplete}
>
<label>{task.desc}</label>
<button onClick={() => deleteTaskHandler(id)}> Delete </button>
</div>
);
};
export default Task;
How are you maintaining the complete status of task in higher components?
Currently you are not initializing the complete state of Task.
If the task object contains the isComplete property, then you can use as shown below
const [isComplete, setIsComplete] = useState(task.isComplete);
however, you also need to update value of completed in task. So, I would suggest to have all lower components as stateless. and maintain the state at Higher component i.e. TaskList
import style from "./Task.module.css";
const Task = ({ task, id, deleteTaskHandler, setTaskCompleteHandler }) => {
const markComplete = () => {
setTaskCompleteHandler(!task.isComplete);
};
return (
<div
className={
task.isComplete ? `${style.task} ${style.completed}` : `${style.task}`
}
onClick={markComplete}
>
<label>{task.desc}</label>
<button onClick={() => deleteTaskHandler(id)}> Delete </button>
</div>
);
};
export default Task;
Implement setTaskCompleteHandler in TaskList and pass is as prop as part of Task component reandering.
Your problem is in position of your delete button, that wrapped by div with makrComplete handler, so then you click on your delete button, markComplete fired too, so your isComplete changed and styles deleted.
To prevent this behavor, you can do a little trick with prevent default. So, try to wrap your deleteTaskHandler in another function like that:
const deleteButtonClickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
deleteTaskHandler(id)
}
and your delete button makrdown should be look like this:
<button onClick={deleteButtonClickHandler}> Delete </button>
I am trying to hide multiple divs with useSate.
They will be rendered random on the page, not from a list.
I have managed to do so by setting up different variables but couldn't find a more generic solution:
https://stackblitz.com/edit/react-t3shrc?file=src%2FApp.js
Also is there a way to close them when clicking outside?
Can you help please.
export default function App() {
const [isVisible, setIsVisible] = useState(false);
const [isVisible2, setIsVisible2] = useState(false);
const showInfo = (e, setIsVisible) => {
e.preventDefault();
setIsVisible(true);
};
const hideInfo = (e, setIsVisible) => {
e.preventDefault();
setIsVisible(false);
};
return (
<div>
<button
onClick={(e) => {
showInfo(e, setIsVisible);
}}
>
Show info 1
</button>
{isVisible && (
<div className="info">
Info 1
<button
onClick={(e) => {
hideInfo(e, setIsVisible);
}}
>
Close
</button>
</div>
)}
<br></br>
<br></br>
<button
onClick={(e) => {
showInfo(e, setIsVisible2);
}}
>
Show info 2
</button>
{isVisible2 && (
<div className="info">
Info 2
<button
onClick={(e) => {
hideInfo(e, setIsVisible2);
}}
>
Close
</button>
</div>
)}
</div>
);
}
I'm not 100% sure what you mean by a more 'generic' solution. Here is what comes to my mind:
First of all, we create a more complex object to basically hold all the variables / sections we encounter and use this as our state.
const initialVisibleAreas = {
area1: true,
area2: false
};
const [visibleAreas, setVisibleAreas] = useState(initialVisibleAreas);
Please note that this is propabably something you want to generate from your data using Object.keys(...) or mapping an array.
Next up, we create the functions for the buttons to use this new state accordingly:
// shows the element by given key
const showInfo = (event, key) => {
event.preventDefault();
setVisibleAreas({ ...visibleAreas, ...{ [key]: true } });
};
// hides the element by given key
const hideInfo = (event, key) => {
event.preventDefault();
setVisibleAreas({ ...visibleAreas, ...{ [key]: false } });
};
// sets every key to false to hide them all at once
const hideAllInfo = (event) => {
event.preventDefault();
const allFalse = Object.assign(
...Object.keys(visibleAreas).map((key) => ({ [key]: false }))
);
setVisibleAreas(allFalse);
};
Last but not least, we use them in jsx. This is basically one 'section':
<button
onClick={(e) => {
showInfo(e, 'area2');
}}
>
Show info 2
</button>
{
visibleAreas['area2'] && (
<div className="info">
Info 2
<button
onClick={(e) => {
hideInfo(e, 'area2');
}}
>
Close
</button>
</div>
);
}
To answer the last question; nothing is holding you to call 'hideAllInfo' inside a onClick handler of your surounding div. Sample is included in the modified stackblitz.
Have a complete look at the modified stackblitz
EDIT: In the case you want to close all areas by clicking the surrounding div, make sure to not propagate the button click event with:
event.stopPropagation();
I updated the stackblitz once again.
I have a list of items (I get the items with a GET-call - I didn't add it here, because I think it's irrelevant). When I delete an item, the list should be updated/ re-rendered.
To do this I use the useEffect-hook with a second parameter (productData).
Problem:
I have to refresh the page manually in order to see the new list of items. I don't understand why: I use useEffect with the second parameter. Could someone point me in the right direction what is wrong? Thanks a lot!
Here is my code:
export default function MemberSavedProducts() {
const [productData, setProductData] = useState([]);
const [successMessage, setSuccessMessage] = useState();
const [errorMessage, setErrorMessage] = useState();
useEffect(() => {}, [productData]);
const deleteProduct = async(prod) => {
try {
if (window.confirm("Are you sure you want to delete this product?")) {
const {
data
} = await fetchContext.authAxios.delete(
`savedProducts/${prod}`
);
setProductData(
productData.filter((prod) => prod !== data.deletedItem.Id)
);
setSuccessMessage(data.message);
}
} catch (err) {
const {
data
} = err.response;
setErrorMessage(data.message);
}
};
return (
<CardLarge>
<div className={styles.productWrapper}>
{successMessage && <SuccessMessage text={successMessage} />}
{errorMessage && <ErrorMessage text={errorMessage} />}
{productData.map((prod) => {
return (
<div
key={prod.id}
>
<ProductItem
prod={prod}
onClick={() => {
getInfo(prod.information);
}}
/>
<button
onClick={() => {deleteProduct(prod.Id)}}
>
Delete
</button>
</div>
);
})}
</div>
</CardLarge>
);
}
Discussed this in the comments, posting this answer for completeness.
Are you sure the filter function works? It seems the refresh works because the GET response returns the right array. I think it should be productData.filter((prod) => prod.Id !== data.deletedItem.Id));. Because in your code you are comparing an object to a string.
Or you can use the passed parameter prod instead of the response maybe like this productData.filter((p) => p.Id !== prod));
Also a small clarification: useEffect does not cause a rerender, changing state does trigger a rerender. useEffect is just a listener/callback that triggers on change of the declared dependencies.
In my React application I have a component called Value, which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).
I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases) which is a parent of each of the instances of the Value component. It has a state called mode, and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.
My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.
Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made?
I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.
Here are the code for my components:
const ModeContext = React.createContext()
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
You first need to create your context before you can use it as a provider or user.
So make sure to add this to the top of the file.
const ModeContext = React.createContext('hideAll')
As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.
If your components are on separate files, make sure to also export ModeContext and import it in the other component.
Example
Here's one way to organize everything and keep it simple.
// cases.js
const ModeContext = React.createContext('hideAll')
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export function useModeContext() {
return useContext(ModeContext)
}
// value.js
import { useModeContext } from './cases.js'
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
P.S. I've made this mistake many times, too.
You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode. In your context, you should provide also a function to hide the components, you can call setHidden
Change the Value component like the following:
export default function Value(props) {
const { mode, setHidden } = useContext(ModeContext)
if(mode === "showAll") {
return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span>
} else if(mode === "hideAll") {
return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
return null;
}
)
}
P.S. Because mode seems a boolean value, you can switch between true and false.
[1]: https://reactjs.org/docs/lifting-state-up.html
There are a few ways to handle this scenario.
Move the state in the parent component. Track all visible states in the parent component like this:
const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
{cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll")
useEffect(() => {
setMode("")
}, [mode])
I have milestoneCards.
I want to add a sort button, that upon clicking this button the cards will be sorted by the card heading.
The sort takes place, but it does not re-render the list in the sorted order.
please advise.
thank you so much for helping me here.
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { Card, CardBody, CardTitle } from "reactstrap";
const MyMilestones = props => {
let sortClicked = false;
let milestoneCards =
props.milestones.length > 0
? props.milestones.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
))
: null;
const sortedMilestoneCards = [...props.milestones]
.sort((a, b) => (a.attributes.heading > b.attributes.heading ? 1 : -1))
.map(m => (
<p key={m.id}>
<Link to={`/milestones/${m.id}`}>{m.attributes.heading}</Link>
</p>
));
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
sortClicked = true;
console.log("before", milestoneCards);
milestoneCards = sortedMilestoneCards;
console.log("after", milestoneCards);
return (milestoneCards = sortedMilestoneCards);
}}
>
Sort
</button>
sortClicked ? ({sortedMilestoneCards}) : {milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
const mapStateToProps = state => {
return {
milestones: state.myMilestones
};
};
export default connect(mapStateToProps)(MyMilestones);
It's because you need to have sortClicked to be tracked by React.
When let sortClicked = false is declared inside MyMilestones component, it's declared once on the first component mount and won't be updated when the component is re-rendered.
So you can save sortClicked in a state using React.useState and update it onClick. useState is a one-off way of storing this.state value for Class Component but for one state. (I won't get into it too deep as React documentation has a thorough coverage on Introducing Hooks)
const MyMilestones = props => {
// let sortClicked = false;
// Initialize it to "false" by default.
let [sortClicked, setSortClicked] = React.useState(false)
let milestoneCards = ...;
const sortedMilestoneCards = ...;
return (
<div className="MilestoneCards">
{
<Card>
<CardBody>
<CardTitle>
<h4>My Milestones</h4>
</CardTitle>
<button
onClick={() => {
// Notify "React" to re-render.
setSortClicked(true)
// No need to return a new reference here.
}}
>
Sort
</button>
{/* 👇 Note that {} is wrapped around the whole block. */}
{sortClicked ? sortedMilestoneCards : milestoneCards}
</CardBody>
</Card>
}
</div>
);
};
It's because you're not updating the milestones correctly. Since they're stored on Redux state, you need to add and dispatch the action that modifies the state.
I recommend you look at the Redux documentation.