I am fairly new to react to please bare with me. I am creating a todo application that just keeps tracks of tasks.
Issue:
the state called [tasks, setTasks] = useState([]) does update when i try to edit a task within the tasks state.
For example lets say tasks contains ["make bed", "fold cloths"] and I wanted to update "fold cloths" to "fold clothes and put away". When I use setTasks the state does not update.
The modification of the state happens in the modifyTask function.
Here is my code:
import React, { useEffect, useState } from "react";
// Imported Components below
import CurrentTasks from "./CurrentTasks";
function InputTask() {
// Initial array for storing list items
// Tracking state of input box
const [task, setTask] = useState("");
const [tasks, setTasks] = useState(
JSON.parse(localStorage.getItem("tasks") || "[]")
);
function deleteTask(index) {
function checkIndex(task, i) {
if (i !== index) {
return task;
}
}
setTasks(prev => prev.filter(checkIndex));
}
function modifyTask(index) {
let editedTask = prompt("Please edit task here..", tasks[index]);
function editTask(task, i) {
if (i === index) {
task = editedTask;
}
console.log(task);
return task;
}
setTasks(prev => prev.filter(editTask));
}
function handleAddTask() {
setTasks(prev => prev.concat(task));
}
useEffect(() => {
// Saves every time tasks state changes
localStorage.setItem("tasks", JSON.stringify(tasks));
console.log("Tasks: " + tasks);
setTask("");
}, [tasks]);
return (
<div className="container">
<div className="input-group mt-4">
<input
type="text"
value={task}
onChange={e => setTask(e.target.value)}
className="form-control form-control-lg"
placeholder="Write task here..."
/>
<div className="input-group-append">
<button onClick={handleAddTask} className="btn btn-outline-secondary">
Add
</button>
</div>
</div>
<CurrentTasks
allTasks={tasks}
deleteTask={deleteTask}
modifyTask={modifyTask}
/>
</div>
);
}
export default InputTask;
What I think is happening
-> My theory is that the state only updates when the number of elements within the array changes. If thats the case is there an elegant way of doing so?
You just need to change your modify function to use map instead of filter, as filter doesn't change the array outside of removing elements. Map takes the returned values and creates a new array from them.
function modifyTask(index) {
let editedTask = prompt("Please edit task here..", tasks[index]);
function editTask(task, i) {
if (i === index) {
task = editedTask;
}
console.log(task);
return task;
}
setTasks(prev => prev.map(editTask));
}
Related
I'm working on a todo app and I have added the functionality to add a task. I am having trouble clearing out the input box and be ready for the next input.
Currently, you can add a todo, it clears the input box, I add another todo, it gets added but the text is missing.
const handleOnClick = (e) => {
e.preventDefault();
console.log("ref.current.value - ", ref.current.value);
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: ref.current.value,
done: false,
},
]);
ref.current.value = ""; // clears it out but cant anything new in
};
In the console log, I can see the text for each todo but it is not getting entered into the array. using useState for the object and merging it with the previous.
Link to code sandbox: https://codesandbox.io/s/cold-darkness-v80pwr?file=/src/Components/AddItem.js
This happens because you are using the ref and changing the value of the element, but you dont have an onChange function that handles it's value, and using the ref in this case just to clear out the value and using it to create a task it's a wrong usage, and you should use a simple useState and set the onChange and value of the input.
Here is the edited sandbox - https://codesandbox.io/s/condescending-bush-gkvs93?file=/src/Components/AddItem.js
The function inside tasks.addTasks(...) called after ref.current.value = "". So you got an empty todo.
You don't need refs in this case. Here is working example:
https://codesandbox.io/s/nameless-frog-sqtldd?file=/src/Components/AddItem.js
import { v4 as uuidv4 } from "uuid";
import React, { useState } from "react";
const AddItem = (tasks, addTasks) => {
const [value, setValue] = useState("");
const handleOnClick = (e) => {
e.preventDefault();
tasks.addTasks((prev) => [
...prev,
{
id: uuidv4(),
todo: value,
done: false
}
]);
setValue("");
};
return (
<div>
<div>
<h2 className="">What needs to be done?</h2>
{<p>{tasks.tasks[0].todo}</p>}
<div className="task-input">
<input
type="text"
className="d-inline mx-2"
placeholder="Add a task"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button className="d-inline mx-2" onClick={handleOnClick}>
Add
</button>
</div>
</div>
</div>
);
};
export default AddItem;
I'm trying to build a game deals watcher and I been working on my browse page. I want to add a filter based on price and title in one fetch request. However, I'm not sure how to accomplish that in my case. Here's my Browse.jsx file:
import React, { useState, useEffect } from 'react';
function Browse({currentUser}) {
const [gameDealsList, setGameDealsList] = useState([]);
const [gameTitle, setTitle] = useState('')
// const [minPrice, setMinPrice] = useState('')
// const [maxPrice, setMaxPrice] = useState('')
const defaultURL = `https://www.cheapshark.com/api/1.0/deals?`
useEffect(()=>{
fetch(defaultURL)
.then((r)=>r.json())
.then((gameList)=> setGameDealsList(gameList))
},[])
console.log(gameDealsList)
function handleRedirect(e, dealID){
e.preventDefault();
window.open(`https://www.cheapshark.com/redirect?pageSize=10&dealID=${dealID}`, '_blank');
return null;
}
return(
<div className="container-fluid">
<h1>Browse</h1>
<h4>Filter:</h4>
<input placeholder='Title' value={gameTitle} onChange={(e)=>setTitle(e.target.value)}></input>
<span>Price Range $:</span>
<input
type="range"
className="price-filter"
min="0"
value="50"
max="100"
/>
<br/><br/>
{gameDealsList.map((game) =>
<div className="container" key={game.dealID}>
<div className="row">
<div className="col">
<img src={game.thumb} className="img-thumbnail" alt='thumbnail'/>
</div>
<div className="col">
<strong><p>{game.title}</p></strong>
</div>
<div className="col">
<span><s>${game.normalPrice}</s></span><br/>
<span>${game.salePrice}</span><br/>
<span>{Math.round(game.savings)}% Off</span>
</div>
<div className="col">
<button onClick={(e)=>handleRedirect(e, game.dealID)}>Visit Store</button>
</div>
<div className="col">
{currentUser ? <button>Add to wishlist</button> : null}
</div>
</div><br/>
</div>
)}
</div>
)
}
export default Browse;
Right now, I'm only fetching deals without any filters. However, the API allows me to set filters. For instance, if I want to search deals based on video game title I can just add &title={random title}. Also, I can type in &upperPrice={maximum price} to set up max price of deals. So, I would like to figure out ways to implement these filters in my fetch request without writing multiple fetch requests.
You can try this approach. Only 1 comment in the code to be worried about.
Another thing to worry is to add a Debounce to a fetch function, because without that requests will be sent every time variables in depsArray changed, so if i try to type Nights - 6 requests will be sent while im still typing.
In order to have everything working well:
Create some utils.js file in order to keep some shared helper functions. For debounce in our case.
utils.js
export function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait);
};
}
Import and wrap the function we want to debounce with useCallback and actual debounce:
import { debounce } from "./utils";
/* ... */
const fetchDeals = useCallback((queryObject) => {
const url = new URL(`https://www.cheapshark.com/api/1.0/deals`);
for (const [key, value] of Object.entries(queryObject)) {
if (value) url.searchParams.append(key, value);
}
console.log(url);
return fetch(url)
.then((r) => r.json())
.then((gameList) => setGameDealsList(gameList));
}, []);
const fetchDealsDebounced = useMemo(() => {
// So API call will not be triggered until 400ms passed since last
// action that may trigger api call
return debounce(fetchDeals, 400);
}, [fetchDeals]);
// Initial call will still be executed but only once, even if we have
// 3 items in depsArray (due to debounce)
useEffect(() => {
// Name object keys according to what API expects
fetchDealsDebounced({ title: gameTitle, upperPrice: maxPrice });
}, [fetchDealsDebounced, gameTitle, maxPrice]);
You should be able to append the query parameters directed by the API to your default query string
fetch(defaultURL + new URLSearchParams({
lowerPrice: minPrice,
upperPrice: maxPrice,
title: gameTitle,
}).then()...
As far as how to control this with only one request you could refactor useEffect like this.
useEffect(() => {
const queryParams = {
lowerPrice: minPrice,
upperPrice: maxPrice,
title: gameTitle,
};
if (minPrice === '') delete queryParams.lowerPrice;
if (maxPrice === '') delete queryParams.upperPrice;
if (gameTitle === '') delete queryParams.title;
fetch(defaultURL + new URLSearchParams(queryParams).then()...
}, [maxPrice, minPrice, gameTitle]);
My application has a modal window with filters. I would like to add another kind of filter to it. I just don’t know how to implement it in React (perhaps you can help me with the code, recommend links).
The meaning is as follows: I want the filters to have a line in which the user could write some value on his own, press Enter, see the entered result (it is possible to enter several values by which you can filter).
Perhaps my explanation is chaotic, in addition I will provide a screenshot with the desired result:
For UI you can use or create a custom input field like this https://evergreen.segment.com/components/tag-input.
For implementation:
Now you have fields searched by user
Ex-
const list = [2,3,4,56,7,8,12,34,0,1]
const searchedItems = [3,7,8]
const finalList= [];
function searchItems() {
list.forEach(item => {
searchedItems.forEach(ele => {
if(ele===item) finalList.push(item);
})
});
return finalList;
}
console.log(searchItems())
Finally use this array in your search item results.
export function ListItem({ itemValue }) {
return (
<div className="item">
<span className="itemValue">{itemValue}</span>
</div>
);
}
export default function App() {
const [itemList, setListItem] = useState([]);
const [item, setItem] = useState("");
const addValue = (event) => {
setItem(event.target.value);
};
const listenEnter = (event) => {
if (event.key === "Enter" && event.target.value !== "") {
setListItem([...itemList, item]);
setItem("");
}
};
return (
<div className="App">
<input
className="inputElement"
placeholder="Enter Value"
value={item}
onChange={addValue}
onKeyUp={listenEnter}
/>
<div className="itemList">
{itemList.map((item) => (
<ListItem itemValue={item} />
))}
</div>
</div>
);
}
whole code https://codesandbox.io/s/elastic-meadow-h5kbxs
I’m running into an error that I could use some help on
Basically, I have a react app that is executing an HTTP call, receiving an array of data, and saving that into a state variable called ‘tasks’. Each object in that array has a key called ‘completed’. I also have a checkbox on the page called ‘Show All’ that toggles another state variable called showAll. The idea is by default all tasks should be shown however if a user toggles this checkbox, only the incomplete tasks (completed==false) should be shown. I can get all tasks to display but can’t get the conditional render to work based on the checkbox click
Here’s how I’m implementing this. I have the HTTP call executed on the page load using a useEffect hook and available to be called as a function from other change handlers (edits etc.)
Before I call the main return function in a functional component, I’m executing a conditional to check the status of ’ShowAll’ and filter the array if it's false. This is resulting in too many re-render errors. Any suggestions on how to fix it?
See simplified Code Below
const MainPage = () => {
const [tasks, setTasks] = useState([]); //tasks
const [showAll, setShowAll] = useState(true); //this is state for the checkbox (show all or just incomplete)
useEffect( ()=> {
axios.get('api/tasks/')
.then( response => { //this is the chained API call
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
}, []);
const fetchItems = (cat_id) => {
axios.get('/api/tasks/')
.then( response => {
setTasks(response.data.tasks);
})
.catch(err => {
console.log('error');
})
};
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(!showAll)
console.log('Checkbox: ', showAll)
};
//this part updates the tasks to be filtered down to just the incomplete ones based on the checkbox value
if (showAll === false) {
setTasks(tasks.filter(v => v['completed']===false)); //only show incomplete tasks
}
return (
<div>
<label className="checkb">
<input
name="show_all"
id="show_all"
type="checkbox"
checked={showAll}
onChange={handleCheckboxChange}
/> Show all
</label>
<br/>
{ tasks && tasks.map((task, index) => {
return (
<div key={index} className="task-wrapper flex-wrapper">
<div >
{ task.completed === false ? (
<span> {index +1}. {task.task_description} </span> ) :
(<strike> {index +1}. {task.task_description} </strike>) }
</div>
<div>
<button
onClick={()=> modalClick(task)}
className="btn btn-sm btn-outline-warning">Edit</button>
<span> </span>
</div>
</div>
)
})}
</div>
);
};
export default MainPage;
Thanks
Two things to fix:
Use the checked property on event.target to update the state:
const handleCheckboxChange = ({target: { checked }}) => {
setShowAll(checked)
};
Filter as you want but don't update the state right before returning the JSX as that would trigger a rerender and start an infinite loop:
let filteredTasks = tasks;
if (!showAll) {
filteredTasks = tasks?.filter(v => !v.completed));
}
and in the JSX:
{ tasks && tasks.map should be {filteredTasks?.map(...
use e.target.value and useEffect :
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
if (!e.target.checked) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
};
or
//change the checkbox state
const handleCheckboxChange = (e) => {
setShowAll(e.target.checked)
console.log('Checkbox: ', showAll)
};
useEffect(()=>{
if (showAll === false) {
let list =tasks.filter(v => v.completed===false);
setTasks(list ); //only show incomplete tasks
}
},[showAll])
I'm trying to display the value of my inputs from a from, in a list. Everytime I hit submit, I expect that it should display the inputs in order.
The problem I'm having is that when I try to submit my form and display inputs in a list, it display an empty value first. On the next submit and thereafter, it displays the previous value, not the new one on the input field.
There's also an error message but i'm not understanding how to relate it to the problem. It's a warning message regarding controlled/uncontrolled components.
I've tried to add if statements to check for empty values in each functions but the problem persists.I've tried to manage the error massage by being consistent with all input to be controlled elements using setState, but nothing works.
I looked through todo list examples on github. I guess i'm trying to keep it in one functional component versus multiple ones, and I'm not using class components. I tried to follow the wesbos tutorial on Javascript 30 day challenge, day 15: Local Storage and Event Delegation. I'm trying to use React instead of plain JS.
Here's what my component looks like.
import React, { useEffect, useState } from "react";
import "../styles/LocalStorage.css";
export const LocalStorage = () => {
const [collection, setCollection] = useState([]);
const [value, setValue] = useState();
const [item, setItem] = useState({ plate: "", done: false });
const [display, setDisplay] = useState(false);
//set the value of the input
const handleChange = (e) => {
if (e.target.value === "") return;
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
if (value === "" || undefined) return;
setItem((prevState) => {
return { ...prevState, plate: value };
});
addItem(item);
setDisplay(true);
setValue("");
};
const addItem = (input) => {
if (input.plate === "") return;
setCollection([...collection, input]);
};
return (
<div>
<div className="wrapper">
<h2>LOCAL TAPAS</h2>
<ul className="plates">
{display ? (
collection.map((item, i) => {
return (
<li key={i}>
<input
type="checkbox"
data-index={i}
id={`item${i}`}
checked={item.done}
onChange={() =>
item.done
? setItem((state) => ({ ...state, done: false }))
: setItem((state) => ({ ...state, done: true }))
}
/>
<label htmlFor={`item${i}`}>{item.plate}</label>
</li>
);
})
) : (
<li>Loading Tapas...</li>
)}
</ul>
<form className="add-items" onSubmit={handleSubmit}>
<input
type="text"
name="item"
placeholder="Item Name"
required
value={value}
onChange={handleChange}
/>
<button type="submit">+ Add Item</button>
</form>
</div>
</div>
);
};
Since the setState function is asynchronous, you cannot use the state value item right after you fire the setItem(...). To ensure you get the latest value for your addItem function:
setItem((prevState) => {
const newItem = { ...prevState, plate: value };
addItem(newItem); // Now, it's getting the updated value!
return newItem;
});
And regarding the controlled and uncontrolled components, you can read the docs about it here. To fix your problem, you can initialize the value state with an empty string:
const [value, setValue] = useState('');