Iam building a quiz-website as practice for React and want to be able to change question with a button. When I change useState(0) manually to 1 the next object renders. But i cant get it to work with the button. When i click the button it jumps straight to the alert message.
function GetMovies() {
useEffect(() => {
fetchItems();
}, []);
const [items, setItems] = useState({ options: [] });
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
//test
//console.log(items[currentQuestion]);
setItems(items[currentQuestion]);
};
const [currentQuestion, setCurrentQuestion] = useState(0);
//change question solution that dont work
const HandleAnswerButtonClick = () => {
const nextQuestion = setCurrentQuestion + 1;
if (nextQuestion < items.length) {
setCurrentQuestion(nextQuestion);
} else {
alert("End of quiz");
}
setCurrentQuestion(nextQuestion);
};
return (
<div className="App">
<h1>Quiza</h1>
<div>
<span>Question 1</span>
</div>
<div>
<h3>Question: {items.description}</h3>
{items.options.map((c) => (
<button value={c.is_correct} key={c.text}>
{c.text}
</button>
))}
<div>
{//Next BUTTON}
<button onClick={() => HandleAnswerButtonClick()}>
Next question
</button>
</div>
{/* if-sats, om bild finns till frågan visas den, annars en class med display none */}
<div className="Q_pics">
{items.options.map((c) =>
!c.image ? (
<p className="Display_none">empty</p>
) : (
<img src={c.image} alt={c.text}></img>
)
)}
</div>
</div>
</div>
);
}
The mongoose schema from my API
const MovieSchema = mongoose.Schema(
{
category: { type: String, required: true },
description: { type: String, required: true },
image: {
type: String,
required: false,
},
options: [
{
text: {
type: String,
required: true,
},
is_correct: {
type: Boolean,
required: true,
default: false,
},
image: {
type: String,
required: false,
},
},
],
},
{ collection: "movies" }
);
That is because
const nextQuestion = setCurrentQuestion + 1;
should be
const nextQuestion = currentQuestion + 1;
Oh, and as #Nick Parsons mentioned in a comment, you also only store the first question in your state, since the fetchItems is only run once, and you do setItems(items[currentQuestion]);
You should do something like
function GetMovies() {
const [items, setItems] = useState([]);
const [currentQuestion, setCurrentQuestion] = useState(0);
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
//test
//console.log(items[currentQuestion]);
setItems(items);
};
//change question solution that dont work
const HandleAnswerButtonClick = () => {
const nextQuestion = currentQuestion + 1;
if (nextQuestion < items.length) {
setCurrentQuestion(nextQuestion);
} else {
alert("End of quiz");
}
setCurrentQuestion(nextQuestion);
};
useEffect(() => {
fetchItems();
}, []);
const activeQuestion = items[currentQuestion];
return (
<div className="App">
<h1>Quiza</h1>
<div>
<span>Question {currentQuestion + 1}</span>
</div>
<div>
<h3>Question: {activeQuestion && activeQuestion.description}</h3>
{activeQuestion && activeQuestion.options.map((c) => (
<button value={c.is_correct} key={c.text}>
{c.text}
</button>
))}
<div>
{//Next BUTTON}
<button onClick={() => HandleAnswerButtonClick()}>
Next question
</button>
</div>
{/* if-sats, om bild finns till frågan visas den, annars en class med display none */}
<div className="Q_pics">
{activeQuestion && activeQuestion.options.map((c) =>
!c.image ? (
<p className="Display_none">empty</p>
) : (
<img src={c.image} alt={c.text}></img>
)
)}
</div>
</div>
</div>
);
}
the code might be like this:
const [items, setItems] = useState({ options: [] });
// currentIdx might be a better name
const [currentIdx, setCurrentIdx] = useState(0);
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
// save all data as Nick said. and items should be type of Data[];
setItems(items);
};
const HandleAnswerButtonClick = () => {
const nextQuestion = currentIdx + 1;
if (nextQuestion < items.length) {
setCurrentIdx(nextQuestion);
} else {
alert("End of quiz");
}
};
// use displayItem for render
const displayItem = useMemo(() => items[currentIdx], [items, currentIdx]);
you'd better learn to use dev tools and watch values of your code I think 😂
Related
bit of a newbie at js here and I'm creating a search filter for my NFT marketplace platform.
I've successfully been able to create a basic input function to search for the name of the item uploaded but because the category of the item is an array, I'm finding difficulty creating checkbox inputs to filter the search results by category.
Here's the piece my code with the search by name function:
export function Results() {
const { fetchNFTs, setError, currentAccount } = useContext(
NFTMarketplaceContext
);
const [nfts, setNfts] = useState([]);
const [nftsCopy, setNftsCopy] = useState([]);
useEffect(() => {
try {
// if (currentAccount) {
fetchNFTs().then((items) => {
setNfts(items.reverse());
setNftsCopy(items);
console.log(nfts);
});
// }
} catch (error) {
setError("Please reload the browser", error);
}
}, []);
const onHandleSearch = (value) => {
const filteredNFTS = nfts.filter(({ name }) =>
name.toLowerCase().includes(value.toLowerCase())
);
if (filteredNFTS.length === 0) {
setNfts(nftsCopy);
} else {
setNfts(filteredNFTS);
}
};
const onClearSearch = () => {
if (nfts.length && nftsCopy.length) {
setNfts(nftsCopy);
}
};
return {
<SearchBar
onHandleSearch={onHandleSearch}
onClearSearch={onClearSearch}
/>
}
And my search bar:
import React, { useEffect, useState } from "react";
const SearchBar = ({ onHandleSearch, onClearSearch }) => {
const [search, setSearch] = useState("");
const [searchItem, setSearchItem] = useState(search);
useEffect(() => {
const timer = setTimeout(() => setSearch(searchItem), 1000);
return () => clearTimeout(timer);
}, [searchItem]);
useEffect(() => {
if (search) {
onHandleSearch(search);
} else {
onClearSearch();
}
}, [search]);
return (
<div className="SearchBar">
<div className="section-filters-bar-actions">
<form className="form">
<div className="form-item split">
<div className="form-input small">
<label for="items-search">Search Items</label>
<input type="text" id="items-search" style={{width:'370px'}}
onChange={(e) => setSearchItem(e.target.value)}
value={searchItem}/>
</div>
</div>
</form>
</div>
</div>
);
}
export default SearchBar;
And finally the array in the upload page / function:
const UloadNFT = ({ uploadToIPFS, createNFT }) => {
const [price, setPrice] = useState("");
const [active, setActive] = useState(0);
const [name, setName] = useState("");
const [website, setWebsite] = useState("");
const [description, setDescription] = useState("");
const [royalties, setRoyalties] = useState("");
const [fileSize, setFileSize] = useState("");
const [category, setCategory] = useState(0);
const [properties, setProperties] = useState("");
const [image, setImage] = useState(null);
const router = useRouter();
const categoryArry = [
{
category: "AI Generated",
},
{
category: "Artwork",
},
{
category: "Audio",
},
{
category: "Collectible",
},
{
category: "Customization",
},
{
category: "Digital Land",
},
{
category: "Gaming",
},
{
category: "Utility",
},
];
return (
<div className="UloadNFT" style={{height:'1350px'}}>
<div style={{display:'inline-grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0rem'}}>
<div className={Style.upload_box} style={{backgroundColor:'var(--primary-white)',padding:'30px 30px 30px 30px'}}>
<div className={formStyle.Form_box_input}>
<label htmlFor="nft">Item Name</label>
<input
type="text"
placeholder="Name your token..."
className={formStyle.Form_box_input_userName}
onChange={(e) => setName(e.target.value)}
style={{border:'1px solid #202020', backgroundColor:'transparent'}}
/>
</div>
// category upload input area
<div className={formStyle.Form_box_input}>
<label htmlFor="name">Choose Category</label>
<p className={Style.upload_box_input_para}>
Choose a specific category for your token
</p>
<br></br>
<div className={Style.upload_box_slider_div}>
{categoryArry.map((el, i) => (
<div
className={`${Style.upload_box_slider} ${
active == i + 1 ? Style.active : ""
}`}
key={i + 1}
onClick={() => (setActive(i + 1), setCategory(el.category))}
>
<p style={{textAlign:'center'}}>{el.category} </p>
</div>
))}
</div>
</div>
<Button
btnName="Create Token"
handleClick={async () =>
createNFT(
name,
price,
image,
description,
router,
category,
// royalties,
// fileSize,
website
// properties
)
}
/>
</div>
</div>
I know it's a lot here since I'm passing the value through multiple functions but I've tried to narrow it down as much as possible. Also the smart contract is working perfectly fine and holds the category variable. I'm sure I just need to call it properly now but it's an array.
Please help!
I tried to replace { name } and name.toLowerCase() with category just to test it but it said 'category.toLowerCase() is undefined'.
Then I tried category.toString().toLowerCase() but it gave the same result.
Function holdAnswer has to change the property isChosen from false to true and mutate state, adding new value to the questions. Then function checkAnswers has to find answers that user has chosen and compare if the selected answers have property isCorrect, which has to be true. And if isCorrect is true and isChosen is true, function checkAnswers has to change the score count. It keeps showing me only the first answers, no matter what the user chose. Why it doesn't show me the answers which user chose?
Please take a look at Codesandbox
App.js
import { useState } from "react";
import QuestionSet from "./components/QuestionSet";
import Answers from "./components/Answers";
import { nanoid } from "nanoid";
function App() {
const [isQuesLoaded, setIsQuesLoaded] = useState(false);
const [questions, setQuestions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [score, setScore] = useState(0);
async function startQuiz() {
try {
setIsQuesLoaded(!isQuesLoaded);
const response = await fetch(
"https://opentdb.com/api.php?amount=5&category=12&difficulty=easy&type=multiple"
);
const data = await response.json();
const allQuestions = data.results;
const listOfQuestions = allQuestions.map((item) => {
const allAnswers = [
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[0]
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[1]
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[2]
},
{
id: nanoid(),
isCorrect: true,
isChosen: false,
answer: item.correct_answer
}
];
return {
id: nanoid(),
question: item.question,
answers: allAnswers
};
});
setQuestions(listOfQuestions);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function holdAnswer(questionId, answerId) {
console.log({ questionId, answerId });
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.id === questionId
? {
...question,
answers: question.answers.map((answer) =>
answer.id === answerId
? { ...answer, isChosen: !answer.isChosen }
: answer
)
}
: question
)
);
}
function checkAnswers() {
let correctCount = 0;
questions.forEach(question => {
const selectedAnswer = question.answers.find(answer => answer.answer.id === answer.answer.isChosen);
if (selectedAnswer.isCorrect) correctCount += 1;
setScore(correctCount);
console.log(correctCount);
console.log(selectedAnswer);
})
}
const questionElm = questions.map((question, index) => {
return (
<section key={index}>
<QuestionSet question={question.question} key={question.id} />
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={(answerId) => holdAnswer(question.id, answerId)}
/>
</section>
);
});
return (
<div className="App">
{!isQuesLoaded ? (
<main>
<h1 className="title-app">Quizzical</h1>
<p className="desc-app">Some description if needed</p>
<button className="btn" onClick={startQuiz}>
Start Quiz
</button>
</main>
) : (
<main className="quest-box">
{loading && <div>Loading data...</div>}
{error && <div>{`There is a problem fetchning data = ${error}`}</div>}
<section className="quest-content">{questionElm}</section>
<button className="answer-btn" onClick={checkAnswers}>Check Answers</button>
</main>
)}
</div>
);
}
export default App;
Answers.js
function Answer(props) {
const styles = {
backgroundColor: props.answer.isChosen ? "#D6DBF5" : "transparent"
};
return (
<div
className="answer-div"
style={styles}
id={props.answer.id}
onClick={() => props.holdAnswer(props.answer.id)}
>
<p>{props.answer.answer}</p>
</div>
);
}
export default function Answers(props) {
return (
<section className="answer-container">
{props.answers.map((answer) => (
<Answer holdAnswer={props.holdAnswer} answer={answer} key={answer.id} />
))}
</section>
);
}
QuestionSet.js
export default function QuestionSet(props) {
return (
<section className="quest" key={props.id}>
<p>{props.question}</p>
</section>
);
}
First thing that I'm seeing is that you are passing holdAnswer function down via props on a wrong way
You need to change to something this (Simplified for convenience):
App.js
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={
holdAnswer(question.id, answerId) // Telling what to execute from here (Without the param on the arrow function)
}
/>
Answers.js
<Answer holdAnswer={
props.holdAnswer // passing the whole function knowing what to execute
}
/>
Answer
<button onClick={() =>
props.holdAnswer // knowing what to do from App.js
}
/>
Or you can do it like this:
App.js
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={
holdAnswer // passing the whole function to execute it in another component
}
/>
Answers.js
<Answer holdAnswer={
props.holdAnswer // passing the whole function to execute it in another
}
/>
Answer
<button onClick={() =>
props.holdAnswer(props.param1, props.param2) // executing the function here passing the parameters
}
/>
Not sure if this is going to fix your problem but at least it can simplify it.
I have the following issue with website where the settings state resets after running more searches. The settings component is show below in the picture, it usually works but if you uncheck a box and then run a few more searches at some point the showAllDividends setting will be set to false, the All dividends component won't be on the screen, but for some reason the checkbox itself is checked (true). This is my first time really working with checkboxes in React, and I think I'm using the onChange feature wrong. Right now I get the event.target.checked boolean, but only onChange.
If that isn't the issue then the most likely cause is the default statements being run again on another render:
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
the thing is I don't see why the default statements would run more than once, the component isn't being destroyed there's no react router usage. I expected it to keep its current state after the page is first loaded. I think the settings defaults are being rerun, but just don't understand why they would.
I unchecked, checked, unchecked the 'All dividends' checkbox, and it was unchecked when I ran 2 more searches. After the second search the checkbox was checked but the component was gone, because showAllDividends was false
main component SearchPage.js:
import React, {useState, useEffect} from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import SearchBar from './SearchBar';
import AllDividendsDisplay from './dividend_results_display/AllDividendsDisplay';
import DividendResultsDisplay from './dividend_results_display/DividendResultsDisplay';
import SettingsView from './settings/SettingsView';
const HOST = process.env.REACT_APP_HOSTNAME
const PROTOCOL = process.env.REACT_APP_PROTOCOL
const PORT = process.env.REACT_APP_PORT
const BASE_URL = PROTOCOL + '://' + HOST + ':' + PORT
const SearchPage = ({userId}) => {
const DEFAULT_STOCK = 'ibm';
const [term, setTerm] = useState(DEFAULT_STOCK);
const [debouncedTerm, setDebouncedTerm] = useState(DEFAULT_STOCK);
const [loading, setLoading] = useState(false);
const [recentSearches, setRecentSearches] = useState([DEFAULT_STOCK]);
const [dividendsYearsBack, setDividendsYearsBack] = useState('3');
const [debouncedDividendYearsBack, setDebouncedDividendYearsBack] = useState('3');
const [errorMessage, setErrorMessage] = useState('');
const [dividendsData, setDividendsData] = useState(
{
current_price: '',
recent_dividend_rate: '',
current_yield: '',
dividend_change_1_year: '',
dividend_change_3_year: '',
dividend_change_5_year: '',
dividend_change_10_year: '',
all_dividends: [],
name: '',
description: '',
}
)
const [settingsViewVisible, setSettingsViewVisible] = useState(false);
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
const onTermUpdate = (term) => {
setTerm(term)
}
// TODO: write a custom hook that debounces taking the term and the set debounced term callback
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedDividendYearsBack(dividendsYearsBack);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [dividendsYearsBack]);
useEffect(() => {runSearch()}, [debouncedTerm]);
useEffect(() => {
// alert(dividendsYearsBack)
if (dividendsYearsBack !== '') {
runSearch();
}
}, [debouncedDividendYearsBack])
useEffect(() => {
console.log("user id changed")
if (userId) {
const user_profile_api_url = BASE_URL + '/users/' + userId
axios.get(user_profile_api_url, {})
.then(response => {
const recent_searches_response = response.data.searches;
const new_recent_searches = [];
recent_searches_response.map(dict => {
new_recent_searches.push(dict.search_term)
})
setRecentSearches(new_recent_searches);
})
.catch((error) => {
console.log("error in getting user profile: ", error.message)
})
}
}, [userId])
useEffect(() => {
const user_profile_api_url = BASE_URL + '/users/' + userId
const request_data = {searches: recentSearches}
axios.post(user_profile_api_url, request_data)
// .then(response => {
// console.log(response)
// })
}, [recentSearches])
const makeSearchApiRequest = () => {
let dividends_api_url = BASE_URL + '/dividends/' + term + '/' + dividendsYearsBack
if (!recentSearches.includes(term)) {
setRecentSearches([...recentSearches, term])
}
axios.get(dividends_api_url, {})
.then(response => {
// console.log(response)
setLoading(false);
setDividendsData(response.data);
})
.catch((error) => {
console.log(error.message);
setLoading(false);
setErrorMessage(error.message);
})
}
const runSearch = () => {
console.log("running search: ", term);
setErrorMessage('');
if (term) {
setLoading(true);
if (!dividendsYearsBack) {
setDividendsYearsBack('3', () => {
makeSearchApiRequest()
});
} else {
makeSearchApiRequest()
}
}
}
const recentSearchOnClick = (term) => {
setTerm(term);
setDebouncedTerm(term);
}
const removeRecentSearchOnClick = (term) => {
const searchesWithoutThisOne = recentSearches.filter(search => search !== term)
setRecentSearches(searchesWithoutThisOne);
}
const dividendsYearsBackOnChange = (text) => {
setDividendsYearsBack(text);
}
const renderMainContent = () => {
if (!debouncedTerm) {
return (
<div className="ui active">
<div className="ui text">Search for info about a stock</div>
</div>
)
}
if (loading === true) {
return (
<div className="ui active dimmer">
<div className="ui big text loader">Loading</div>
</div>
)
}
if (errorMessage) {
return (
<div className="ui active">
<div className="ui text">{errorMessage}</div>
</div>
)
} else {
return (
<DividendResultsDisplay
data={dividendsData}
dividends_years_back={dividendsYearsBack}
dividendsYearsBackOnChange={dividendsYearsBackOnChange}
showMainInfo={showMainInfo}
showYieldChange={showYieldChange}
showAllDividends={showAllDividends}/>
)
}
}
// https://stackoverflow.com/questions/38619981/how-can-i-prevent-event-bubbling-in-nested-react-components-on-click
const renderRecentSearches = () => {
return recentSearches.map((term) => {
return (
<div key={term}>
<button
onClick={() => recentSearchOnClick(term)}
style={{marginRight: '10px'}}
>
<div>{term} </div>
</button>
<button
onClick={(event) => {event.stopPropagation(); removeRecentSearchOnClick(term)}}>
X
</button>
<br/><br/>
</div>
)
})
}
const renderSettingsView = (data) => {
if (settingsViewVisible) {
return (
<SettingsView data={data} />
)
} else {
return null;
}
}
const toggleSettingsView = () => {
setSettingsViewVisible(!settingsViewVisible);
}
const toggleDisplay = (e, setter) => {
setter(e.target.checked)
}
const SETTINGS_DATA = [
{
label: 'Main info',
id: 'main_info',
toggler: toggleDisplay,
setter: setShowMainInfo
},
{
label: 'Yield change',
id: 'yield_change',
toggler: toggleDisplay,
setter: setShowYieldChange
},
{
label: 'Dividends list',
id: 'all_dividends',
toggler: toggleDisplay,
setter: setShowAllDividends
},
]
console.log("showMainInfo: ", showMainInfo);
console.log("showYieldChange: ", showYieldChange);
console.log("showAllDividends: ", showAllDividends);
return (
<div className="ui container" style={{marginTop: '10px'}}>
<SearchBar term={term} onTermUpdate={onTermUpdate} />
{renderRecentSearches()}
<br/><br/>
<button onClick={toggleSettingsView}>Display settings</button>
{renderSettingsView(SETTINGS_DATA)}
<div className="ui segment">
{renderMainContent()}
</div>
</div>
)
}
const mapStateToProps = state => {
return { userId: state.auth.userId };
};
export default connect(
mapStateToProps
)(SearchPage);
// export default SearchPage;
the settingsView component:
import React from 'react';
import SettingsCheckbox from './SettingsCheckbox';
const SettingsView = ({data}) => {
const checkboxes = data.map((checkbox_info) => {
return (
<div key={checkbox_info.id}>
<SettingsCheckbox
id={checkbox_info.id}
label={checkbox_info.label}
toggler={checkbox_info.toggler}
setter={checkbox_info.setter}/>
<br/>
</div>
)
});
return (
<div className="ui segment">
{checkboxes}
</div>
);
}
export default SettingsView;
SettingsCheckbox.js:
import React, {useState} from 'react';
const SettingsCheckbox = ({id, label, toggler, setter}) => {
const [checked, setChecked] = useState(true)
return (
<div style={{width: '200px'}}>
<input
type="checkbox"
checked={checked}
id={id}
name={id}
value={id}
onChange={(e) => {
setChecked(!checked);
toggler(e, setter);
}} />
<label htmlFor="main_info">{label}</label><br/>
</div>
);
}
export default SettingsCheckbox;
I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}
I want to update values within an Array of Objects saved to my localStorage.
The Objects are already in my localStorage. I want to update the progress.
The key is skills and the value is the Array.
I have a form with two sliders on my page which tracks the time(progress). On Submit I want to update the progress.
I think I have a major misunderstanding of how this works because I don't get it to work.
[
{category: "crafting"
description: "Prepare yourself for cold times"
imageSrc: "https://images.unsplash.com/photo-1621490153925-439fbe544722?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1100&q=80"
isDone: false
progress: 500
title: "Knitting"},
{category: "mental"
description: "Take control over your mind"
imageSrc: "https://images.unsplash.com/photo-1554244933-d876deb6b2ff?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1100&q=80"
isDone: false
progress: 500
title: "Meditation"}]
This is my handleSubmit function which updates the Value. Its controlled by a form with 2 Sliders (mins) (hrs)
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
const hrs = parseInt(hours);
const mins = parseInt(minutes);
const addedHours = value + hrs + mins * 0.015;
setValue(addedHours);
}
Here is a screenshot of my localStorage:
localStorage
Here is the whole code of the page:
export default function DetailPage(): JSX.Element {
const [hours, setHours] = useState('0');
const [minutes, setMinutes] = useState('0');
const [value, setValue] = useState(0);
const history = useHistory();
const { skills } = useLocalStorageSkills();
const { title }: { title: string } = useParams();
const filteredSkills = skills.filter(
(skills) => skills.title === String(title)
);
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
const hrs = parseInt(hours);
const mins = parseInt(minutes);
const addedHours = value + hrs + mins * 0.015;
setValue(addedHours);
}
const ranking =
value === 0
? '0'
: value < 99
? '1'
: value > 100 && value < 199
? '2'
: value > 200 && value < 299
? '3'
: value > 300 && value < 399
? '4'
: '5';
const ranktrack = JSON.stringify(ranking);
localStorage.setItem('ranking', ranking);
const ranktrackparsed = JSON.parse(ranktrack);
localStorage.getItem('ranking');
return (
<div className={styles.container}>
{filteredSkills.map((skills) => (
<Header
{...skills}
key={skills.title}
title={skills.title}
type="detail"
imageSrc={skills.imageSrc}
onClick={() => {
history.push('/');
}}
/>
))}
<main className={styles.main}>
{filteredSkills.map((skills) => (
<ProgressTrack
value={(skills.progress - value).toFixed(1)}
rank={ranktrackparsed}
/>
))}
<form className={styles.form} onSubmit={handleSubmit}>
<Rangeslider
size="hours"
value={hours}
min={'0'}
max={'24'}
onChange={(event) => setHours(event.target.value)}
/>
<Rangeslider
size="minutes"
value={minutes}
min={'0'}
max={'59'}
onChange={(event) => setMinutes(event.target.value)}
/>
<ActionButton
children={'Submit'}
type={'submit'}
style="primary"
></ActionButton>
</form>
{filteredSkills.map((skills) => (
<ProgressBar
percentageVal={value}
textVal={value.toFixed(1)}
minValue={1}
maxValue={500}
children={skills.description}
/>
))}
</main>
<Navigation activeLink={'add'} />
</div>
);
}
Here I have a custom hook, to use localStorage:
import useLocalStorage from './useLocalStorage';
import type { Skill } from '../../types';
export default function useLocalStorageSkills(): {
skills: Skill[];
addSkill: (skills: Skill) => void;
removeSkill: (newSkill: Skill) => void;
editSkill: (oldSkill: Skill, newSkill: Skill) => void;
} {
const [skills, setSkills] = useLocalStorage<Skill[]>('skills', []);
function addSkill(skill: Skill) {
setSkills([...skills, skill]);
}
function removeSkill(newSkill: Skill) {
setSkills(skills.filter((skill) => skill !== newSkill));
}
function editSkill(deleteSkill: Skill, newSkill: Skill) {
setSkills([
...skills.filter((skill) => skill.title !== deleteSkill.title),
newSkill,
]);
}
return { skills, addSkill, removeSkill, editSkill };
}
Its my first question, If you need more information, I will do my best to provide more.