Adding Local Storage option in my ToDo List site - reactjs

I made my basic ToDo list site after learning react. I wanted to add a function of saving the items on reload. I am a beginner in react so I am facing difficulty in this. I tried the following code:
import React from "react";
import ToDoList from "./components/ToDoList";
import Navbar from './components/Navbar'
import '../src/App.css'
export default function TodoInput() {
const saveLocalTasks = () => {
let savedTasks = localStorage.getItem('tasks')
console.log(savedTasks)
if (savedTasks) {
return JSON.parse(localStorage.getItem('tasks'))
} else {
return []
}
}
const [task, setTask] = React.useState('')
const [count, setCount] = React.useState(0)
const [taskList, setTaskList] = React.useState([saveLocalTasks()])
const [disable, setDisable] = React.useState(true)
const [viewTaskList, setViewTaskList] = React.useState(true)
const updateTaskList = () => {
setTaskList([...taskList, {object: task, key: Date.now()}])
setTask('')
setViewTaskList(false)
setCount(count + 1)
setDisable(true)
}
const inputValue = e => {
setTask(e.target.value)
e.target.value === '' || task === '' || task.length === 0
?
setDisable(true)
:
setDisable(false)
}
// console.log(task.length)
React.useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(taskList.object))
}, [taskList])
return (
<div>
<Navbar />
<header>
<div className="todolist-border">
<div className="todo-input-form">
<input
className = "inputText"
placeholder="Add a Task"
value={task}
onChange = {inputValue}
/>
<button disabled = {disable} onClick = {updateTaskList} className="todo-add-button">+</button>
</div>
{
viewTaskList || count === 0
?
<div className="pendingTasks-div">
<img className = "pending-task-image"
src= "https://dm0qx8t0i9gc9.cloudfront.net/watermarks/image/rDtN98Qoishumwih/task-pending-cartoon-business-vector-illustrations_zJCs81OO_SB_PM.jpg"
alt="pending-tasks" />
<p className="no-task-message">There are no pending tasks!! #Enjoy🥳🥳</p>
</div>
:
<ToDoList count = {count} setCount = {setCount} task = {task} taskList = {taskList} setTaskList = {setTaskList}/>
}
</div>
</header>
</div>
)
}
But the following error is coming up:
The following is the code for ToDoList component:
import React from "react";
export default function ToDoList(props) {
const deleteTaskListItem = (key) => {
const updatedList = props.taskList.filter((item) => {
return (
item.key !== key
)
})
props.setTaskList(updatedList)
props.setCount(props.count - 1)
}
return(
<div>
{props.taskList.map((item) => {
return (
<div key = {item.key} className="todolist-div">
<input type="checkbox" className="list-checkbox">
</input>
<p>{item.object}</p>
<button onClick={()=>deleteTaskListItem(item.key)} className="delete-button">X</button>
</div>
)
})}
</div>
)
}
Kindly suggest a method to add this feature.

The above error happens when you try to JSON.parse undefined. Check this link. Here, I tried to do some changes in your code on CodeSandbox. There you can find some changes I have made.
Firstly, you shouldn't try to set data in this useState const [taskList, setTaskList] = React.useState([saveLocalTasks()]). You should set data in useEffect.
In the following code, you are trying to save taskList.object but taskList is an array. The below code will throw an error.
React.useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(taskList.object))
}, [taskList])
As you asked in your question, you want to try to save data when the user reloads the window. You can achieve this by using window.onbeforeunload event (line 48).
Hope the above will help you.
P.S: The codesandbox code I shared isn't fully functional. I have made just some changes that will help you to go ahead with your coding. Thank you.

Related

how can I update a nested object state so that is renders the new state after I click it?

YOu can find this code and the data here: git clone https://github.com/Cerezze/ChallengesDestrib2
This program displays a static array of 7 elements. When clicking the button unlock, I want to make it so the taskName appears next to unlock right after the click. But Im not sure why my prevState does not work. You can find the problem at the bottom of getOneRandomTask I think I am setting the user state wrong. But How can I setUser() so that my state updates immediatly after the click? It is a nested object and array so its very confusing I was hoping for some insight. Thanks in advance for all advice.
import { challengeBucket } from "./DB/challengeBucket";
import { user } from "./DB/user";
import { useEffect, useState } from "react";
function App() {
const [u, setUser] = useState({});
const [cb, setChallengeBucket] = useState([]);
const [dayTasks, setDayTasks] = useState([]);
const [update, setUpdate] = useState(false);
useEffect(()=>{
if(localStorage.getItem('user') !== null && localStorage.getItem('challengeBucket') !== null){
setUser({...JSON.parse(localStorage.getItem('user'))});
setChallengeBucket([...JSON.parse(localStorage.getItem('challengeBucket'))]);
setDayTasks([...JSON.parse(localStorage.getItem('user')).challengeOneDays.dayTasks]);
}
},[update]);
const getOneRandomTask = (i) =>{
let chalBuck = cb;
let randomNumber = Math.floor(Math.random() * 6);
let user = u;
let filteredArr = chalBuck.filter((i, index)=>{
return index === randomNumber;
});
user.challengeOneDays.dayTasks[i].taskName = filteredArr[0].taskName;
localStorage.setItem('user', JSON.stringify(user));
setUser(prevState => {
return {...prevState}}); // here is where I am most likely setting state wrong
}
const setMockStorage = () =>{
localStorage.setItem('challengeBucket', JSON.stringify(challengeBucket));
localStorage.setItem('user', JSON.stringify(user));
}
const deleteMockStorage = () =>{
localStorage.removeItem('challengeBucket');
localStorage.removeItem('user');
}
return (
<div>
<button onClick = {setMockStorage}>Set Storage</button>
<button onClick = {deleteMockStorage}>Delete Storage</button>
{dayTasks.map(i => {
return (
<div key={Math.random()}>
<span>
Day {i.day}
</span>
<button onClick={()=>getOneRandomTask(i.day - 1)} disabled = {i.lockStatus === "locked"?true: false}>
{i.lockStatus}
</button>
<span>
{i.taskName}
</span>
</div>
)
})}
</div>
);
}
export default App;

React listen to child's state from parent

Damn, two days, two noob questions, sorry guys.
Yesterday, I spent the whole afternoon reading the docs but my fart-ey brain cannot process how to use react hooks to pass data from a child to a parent.
I want to create a button on my parent that can listen to his child's state to check on it and change the background color depending on its value.
Thing is, the child component is mapping some stuff so I cannot create a button (otherwhise it would be rendered multiple times and not only once like I want).
I've thought about moving all the data to my parent component but I cannot understand how since I'm fairly new to React and it's been only two months of learning how to code for me basically.
I will now provide the code for the parent and the child component.
The parent :
import React from "react";
import Quizz from "./components/Quizz";
export default function App() {
const [quizz, setQuizz] = React.useState([]);
React.useEffect(() => {
async function getData() {
const res = await fetch(
"https://opentdb.com/api.php?amount=5&category=27&type=multiple"
);
const data = await res.json();
setQuizz(data.results)
}
getData();
}, []);
function checkOnChild(){ /* <== the function I'd like to use to check on my Quizz component's "activeAnswer" state */
console.log(quizz);
}
const cards = quizz.map((item, key) => {
return <Quizz {...item} key={key}/>;
});
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button> /* <== the button that will use the function */
</div>
);
}
and the child :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');/* <== the state I'd like to check on from my parent component */
function toggle(answer) {
setActiveAnswer(answer);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const answerDiv = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
console.log(answers);
console.log(activeAnswer);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
);
}
I'm really sorry If it seems dumb or if I'm making forbidden things lmao, but thanks in advance for your help guys!
This should get you a bit further I think.
export default function App() {
const [quizData, setQuizData] = useState([])
const [quizState, setQuizState] = useState({})
useEffect(() => {
async function getData() {
const res = await fetch('https://opentdb.com/api.php?amount=5&category=27&type=multiple')
const data = await res.json()
const results = data.results
setQuizData(results)
setQuizState(results.reduce((acc, curr) => ({ ...acc, [curr.question]: '' }), {}))
}
getData()
}, [])
function checkOnChild() {
console.log(quizState)
}
const cards = quizData.map((item) => {
return <Quizz {...item} key={item.question} quizState={quizState} setQuizState={setQuizState} />
})
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button>
</div>
)
}
export default function Quizz(props) {
function handleOnClick(answer) {
props.setQuizState(prevState => ({
...prevState,
[props.question]: answer,
}))
}
const answers = useMemo(() => {
const arr = [...props.incorrect_answers, props.correct_answer]
return shuffleArray(arr)
}, [props.incorrect_answers, props.correct_answer])
const answerDiv = answers.map((answer) => (
<div
className="individuals"
key={answer}
onClick={() => handleOnClick(answer)}
style={{ background: answer == props.quizState[props.question] ? '#D6DBF5' : 'transparent' }}
>
{answer}
</div>
))
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
)
}

React Context. how to avoid "Cannot read properties of undefined" error before having a value

I am learning React, and trying to build a photo Album with a a modal slider displaying the image clicked (on a different component) in the first place.
To get that, I set <img src={albums[slideIndex].url} /> dynamically and set slideIndex with the idof the imgclicked , so the first image displayed in the modal slider is the one I clicked.
The problem is that before I click in any image albums[slideIndex].urlis obviously undefined and I get a TypeError :cannot read properties of undefined
How could I solve that?
I tried with data checks with ternary operator, like albums ? albums[slideIndex].url : "no data", but doesn't solve it.
Any Ideas? what i am missing?
this is the component where I have the issue:
import React, { useContext, useEffect, useState } from "react";
import { AlbumContext } from "../../context/AlbumContext";
import AlbumImage from "../albumImage/AlbumImage";
import "./album.css";
import BtnSlider from "../carousel/BtnSlider";
function Album() {
const { albums, getData, modal, setModal, clickedImg } =
useContext(AlbumContext);
console.log("clickedImg id >>", clickedImg.id);
useEffect(() => {
getData(); //-> triggers fetch function on render
}, []);
///////////
//* Slider Controls
///////////
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
console.log("SlideINDEx", slideIndex ? slideIndex : "no hay");
const nextSlide = () => {
if (slideIndex !== albums.length) {
setSlideIndex(slideIndex + 1);
} else if (slideIndex === albums.length) {
setSlideIndex(1);
}
console.log("nextSlide");
};
const prevSlide = () => {
console.log("PrevSlide");
};
const handleOnclick = () => {
setModal(false);
console.log(modal);
};
return (
<div className="Album_Wrapper">
<div className={modal ? "modal open" : "modal"}>
<div>
<img src={albums[slideIndex].url} alt="" />
<button className="carousel-close-btn" onClick={handleOnclick}>
close modal
</button>
<BtnSlider moveSlide={nextSlide} direction={"next"} />
<BtnSlider moveSlide={prevSlide} direction={"prev"} />
</div>
</div>
<div className="Album_GridContainer">
{albums &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
</div>
);
}
export default Album;
THis is my AlbumContext :
import React, { createContext, useState } from "react";
export const AlbumContext = createContext();
export const AlbumContextProvider = ({ children }) => {
const [albums, setAlbums] = useState();
const [modal, setModal] = useState(false);
const [clickedImg, setClickedImg] = useState("");
const showImg = (img) => {
setClickedImg(img);
setModal(true);
console.log(clickedImg);
};
const getData = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/albums/1/photos"
);
const obj = await response.json();
console.log(obj);
setAlbums(obj);
} catch (error) {
// console.log(error.response.data.error);
console.log(error);
}
};
console.log(`Albums >>>`, albums);
return (
<AlbumContext.Provider
value={{ albums, getData, showImg, modal, setModal, clickedImg }}
>
{children}
</AlbumContext.Provider>
);
};
Thanks very much in advance
Your clickedImg starts out as the empty string:
const [clickedImg, setClickedImg] = useState("");
And in the consumer, you do:
const [slideIndex, setSlideIndex] = useState(clickedImg.id);
So, it takes the value of clickedImg.id on the first render - which is undefined, because strings don't have such properties. As a result, both before and after fetching, slideIndex is undefined, so after fetching:
albums ? albums[slideIndex].url : "no data"
will evaluate to
albums[undefined].url
But albums[undefined] doesn't exist, of course.
You need to figure out what slide index you want to be in state when the fetching finishes - perhaps start it at 0?
const [slideIndex, setSlideIndex] = useState(0);
maybe because your code for checking albums is empty or not is wrong and its always return true condition so change your code to this:
<div className="Album_GridContainer">
{albums.length > 0 &&
albums.map((item, index) => {
return (
<AlbumImage
className="Album_gridImage"
key={index}
image={item}
/>
);
})}
</div>
change albums to albums.length

How to prepending before icon for text area for React component

I'm trying to implement React version of TextArea which appends "$" every-time I press Return/Enter.
I'm having hard time prepending a sign # or % or dollar every time someone presses enter. How can I go about this?
This is my basic attempt but I'm kind of lost:
const MyComponent = () => {
const [name, setName] = React.useState('');
return (
<div>
<textarea value={name} onChange={(e) => { setName(e.target.value) }} />
</div>
);
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
Ok so I had a bit of time on my hands and thought this could be a good learning experience. So I present to you: MoneyInputList
import './css/MoneyInputList.css'
import { useState, useEffect } from 'react';
let latestAdded = 'moneyInputList-input-0';
let lastSize = 0;
const MoneyInputList = () => {
const [recordList, setRecordList] = useState({data: ['']});
const handleFormSubmit = (e) => {
e.preventDefault();
if(recordList.data[recordList.data.length-1] !== ''){
setRecordList({data: [...recordList.data, '']})
}
};
useEffect(() => {
if(lastSize !== recordList.data.length)
document.getElementById(latestAdded).focus();
lastSize = recordList.data.length;
}, [recordList]);
return (
<form autoComplete='off' onSubmit={handleFormSubmit}>
<div className="main-container">
{recordList.data.length > 0 &&
recordList.data.map((record, iter) => {
latestAdded = "moneyInputList-input-"+iter;
return (
<div key={"moneyInputList-field-"+iter} className="record-field">
<div className="record-sign">$</div>
<input className="record-input" id={"moneyInputList-input-"+iter} value={recordList.data[iter]} onChange={(e) => {
if(e.target.value === '' && iter !== recordList.data.length-1){
let modifiedData = [];
recordList.data.forEach((e,i) => {
if(i !== iter)
modifiedData.push(e);
});
setRecordList({data: modifiedData});
return;
}
const filteredValue = e.target.value.split('').filter(e=>(e.charCodeAt() >= '0'.charCodeAt() && e.charCodeAt() <= '9'.charCodeAt()));
let formattedValue = [];
filteredValue.forEach((elem, i) => {
if((filteredValue.length - i) % 3 === 0 && i !== 0)
formattedValue.push(',');
formattedValue.push(elem);
});
formattedValue = formattedValue.join('');
e.target.value = formattedValue;
let myData = recordList.data;
myData[iter] = e.target.value;
setRecordList({data: myData});
}} type="text"/>
</div>
)})
}
</div>
<input style={{flex: 0, visibility: 'collapse', height: 0, width: 0, padding: 0, margin: 0}} type="submit"/>
</form>
)
}
export default MoneyInputList;
This component should do what you need it to do. It is not the best code but it works. You can see it working here. Of course you might still need to change some stuff in order for it to fit in your codebase and maybe implement redux, but the general idea is here. You use it by typing whatever number you want pressing enter will create a new line and deleting the content of a line will remove it.
I hope I understood correctly what you are trying to do, here is a super scuffed version of it. You might need to change the code a bit to fit your use case.
import { useState, useEffect } from "react";
export default function App() {
const [textValue, setTextValue] = useState("");
const [displayedTextValue, setDisplayedTextValue] = useState("");
useEffect(() => {
let splitTextValue = textValue.split("\n");
splitTextValue = splitTextValue.map((line, iter) => {
if (iter === splitTextValue.length - 1) return line;
if (line[0] !== "$") return "$ " + line;
return line;
});
setDisplayedTextValue(splitTextValue.join("\n"));
}, [textValue]);
return (
<div>
<textarea
value={displayedTextValue}
onChange={(e) => {
setTextValue(e.target.value);
}}
/>
</div>
);
}
Here is a version working with key event that I think is cleaner when handling thing depending on keys.
Here is the repro on Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const enterKeyCode = 'Enter';
const backspaceKeyCode = 'Backspace';
const [val, setVal] = React.useState('$ ');
const [keyCodeEvent, setKeyCodeEvent] = React.useState();
React.useEffect(() => {
if (keyCodeEvent) {
// Handle numpad 'enter' key
if (keyCodeEvent[0].includes(enterKeyCode)) {
setVal(`${val}\n$ `);
} else if (keyCodeEvent[0] === backspaceKeyCode) {
setVal(`${val.slice(0, -1)}`);
} else {
setVal(`${val}${keyCodeEvent[1]}`);
}
}
}, [keyCodeEvent]);
return (
<div>
{/* Empty onChange to prevent warning in console */}
<textarea onKeyDown={e => setKeyCodeEvent([e.code, e.key])} value={val} onChange={() => {}} />
</div>
);
};
render(<App />, document.getElementById('root'));
I read your comments on Marko Borković 's answer so I handled the backspace but he is totally right when saying you should build a special component for this. It will be way easier to improve and cleaner. You are not safe from some others bugs if you want to add features to your component.

How can I change 2 different variables independently with a single function in React?

I'm new to React and I have a short and stupid question, but my poor phrasing makes it so that I haven't been able to find the answer by searching for it.
Basically, I have 2 password fields. I want to show and hide each one independently, but I would like a more elegant way than having 2 different functions with their own variables like this:
const [showPassword1, setShowPassword1] = useState(false);
const [showPassword2, setShowPassword2] = useState(false);
const togglePasswordVisiblity1 = () => {
setShowPassword1(showPassword1 ? false : true);
};
const togglePasswordVisiblity2 = () => {
setShowPassword2(showPassword2 ? false : true);
};
With the respective buttons below:
<span onClick={togglePasswordVisiblity1}>Show/Hide</span>
<span onClick={togglePasswordVisiblity2}>Show/Hide</span>
I'm sure there's a way to regroup these into a single function that changes the right variable based on which span is clicked, but I haven't had any luck finding the syntax. Sorry again for this question, hopefully it can be answered quickly!
Thanks in advance for your help.
You can try to use an array for the state. Check this sandbox demo:
import React, { useState } from "react";
export default function App() {
const [showPassword, setShowPassword] = useState([false, false]);
return (
<div className="App">
<button
onClick={() => setShowPassword([!showPassword[0], showPassword[1]])}
>
{JSON.stringify(showPassword[0])}
</button>
<button
onClick={() => setShowPassword([showPassword[0], !showPassword[1]])}
>
{JSON.stringify(showPassword[1])}
</button>
</div>
);
}
Refactored version by extracting state update into a function:
import React, { useState } from "react";
export default function App() {
const [showPassword, setShowPassword] = useState([false, false]);
const togglePassword = (idx) => {
const newShowPassword = [...showPassword];
newShowPassword[idx] = !newShowPassword[idx]; // toggle
setShowPassword(newShowPassword); // update the state
};
return (
<div className="App">
<button onClick={() => togglePassword(0)}>
{JSON.stringify(showPassword[0])}
</button>
<button onClick={() => togglePassword(1)}>
{JSON.stringify(showPassword[1])}
</button>
</div>
);
}
const [state , setState] = useState({
showPassword1:false,
showPassword2: false
})
const togglePasswordVisiblity1= e => {
const {name , value} = e.target
setState( prevState => ({
...prevState,
[name]: prevState[name] ? false : true
}))
}
//
<span name='showPassword1' onClick={togglePasswordVisiblity1}>Show/Hide</span>
<span name='showPassword2' onClick={togglePasswordVisiblity1}>Show/Hide</span>

Resources