Custom Hook and cascading dropdown - reactjs

I'm new to react and I'm learning by doing. Right now I'm stacked on using a custom Hook to share logic among components.
here is the logic inside the custom Hook
const useDropDownLogic = () => {
const { menu } = useContext(MenuContext)
const brand = [...new Set(menu.map(i => i.brand))]
const [category, setCategory] = useState([])
const brandChange = (e) => {
const { type, textContent } = e.target
setCategory([...new Set(menu.filter(i => i.[type] == textContent).map(i => i.category))])}
return {
brandChange,
brand,
category
}
}
export default useDropDownLogic;
here is the first component consuming the custom Hook
const DropDownBrand = () => {
const {brandChange, brand} = useDropDownLogic();
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
return (
<div id="dropdownbrand">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{brand.map((i) => (<li key={nanoid()} type = 'brand' value={i} onClick = {(e) => brandChange(e)}>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownBrand;
and here the second
const DropDownCategory = () => {
const { category } = useDropDownLogic()
const dropdownRef = useRef(null);
const [isActive, setIsActive] = useDetectOutsideClick(dropdownRef, false);
const onClick = () => setIsActive(!isActive)
console.log(category);
return (
<div id="dropcategory">
<div className="menu-container menu-brand">
<label className = 'nav-label'>Select a Brand</label>
<button onClick={onClick} className="menu-trigger brand-btn">
<i className="fas fa-chevron-down fa-2x drop-arrow"></i>
</button>
<nav
ref={dropdownRef}
className={`drop-menu ${isActive ? "active" : "inactive"}`}
>
<ul>
{category.map((i) => (<li key={nanoid()} type = 'category'>{i}</li>))}
</ul>
</nav>
</div>
</div>
);
};
export default DropDownCategory;
Basically I'm able to populate the first dropdown but not the second. I don't understand why the category state is not update into the DropDownCategory component
any hint in what I'm doing wrong?
Many thanks in advance

I see,
I'm changing the approach. Moving state to a parent component and passing down function to handle click event. Then on Parent I'll update the dstate and pass it as props down to the child.
Nyhting to share yet but it will come

There is no persisted state in a hook, it's a way to share logic and functions. Because you are defining
const [category, setCategory] = useState([])
inside of the hook, it is defined each time you call the hook. In your case, dropdowncategory and dropdownbrand each have their personal state variable called category.
To solve this, define [category, setCategory] inside of the menu context, (or any other context) and pull in the values into your hook.. That way all instances of useDropDownLogic() are associated to global context value.

Related

How set state for one individual component React when having multiple components

I have this Navbar with 3 tabs, and I managed to build a hook that sets a different style when clicked (changing its class); however, i don't know how target a state directly to just one tab. When clicked, all of then change their states. how I use the "this" in react in a case like this
const [isActive, setIsActive] = useState(false);
const handleClick = () => {
setIsActive(current => !current);
};
const setName = () => {
return (isActive ? 'true' : 'false');
}
return (
<NavStyled>
<div className="navbar-div">
<nav className="nav">
<p className={setName()} onClick={handleClick} >Basic</p>
<p className={setName()} onClick={handleClick} >Social</p>
<p className={setName()} onClick={handleClick} >Certificates</p>
</nav>
</div>
</NavStyled>
);
};
export default Navbar; ```
Navbar is a function component, not a hook.
You need to store either the currentTabIndex or currentTabName in the state.
var [currentTabName, setCurrentTabName] = useState('Basic');
handleClick=(evt)=> {
setCurrentName(evt.target.textContent);
};
['Basic','Social','Certificates'].map((tabName, i)=> {
let clazz = (tabName == currentTabName)?'active':'';
return <p key={tabName} className={clazz} onClick={handleClick} >{tabName}</p>
});

Toggle class on a mapped element in react

I am learning react and I have some doubts about changing the color onClick
How can I toggle the className of a <p> element that is being clicked? At the moment clicking on one changes the color on everything.
I have read about passing a state on a parent element but at this point is getting a bit confusing and if someone could help me clarifying this would be nice.
Here's the code of my child element:
import "./Answer.scss";
function Answer(props) {
const getAnswers = [];
const correct_answer = props.correct_answer;
getAnswers.push(correct_answer);
for (let i = 0; i < props.incorrect_answers.length; i++) {
getAnswers.push(props.incorrect_answers[i]);
}
const [answers, setAnswer] = useState(getAnswers);
const [active, setActive] = useState(true);
const displayAnswers = answers.map((answer, index) => (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer.replace(/"|'/g, '"')}
</p>
));
return <div className="answer-box">{displayAnswers}</div>;
}
export default Answer;
And this is the parent:
import Answer from "../Answer/Answer";
function Questions(props) {
const questions = props.questions.map((question, index) => {
return (
<div className="question" key={index}>
<h2>{question.question.replace(/"|'/g, '"')}</h2>
<Answer
incorrect_answers={question.incorrect_answers}
correct_answer={question.correct_answer}
/>
<hr></hr>
</div>
);
});
return <div className="questions-container">{questions}</div>;
}
export default Questions;
Thanks everyone
Check this code.
const [answers, setAnswer] = useState(getAnswers);
const [active, setActive] = useState(true);
const displayAnswers = answers.map((answer, index) => (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer.replace(/"|'/g, '"')}
</p>
));
You are iterating your answers and updating the same state variable for each answer. It's like you are over-writing the updated values. Instead, you can make a separate state variable for each and every option. Based on the onClick, you can update that specific answer state and use it in the code. Check the below code.
const AnswerText = ({ valid, index, answer }) => {
const [active, setActive] = useState(valid);
return (
<p
className={active ? "answer" : "answer-active"}
key={index}
onClick={() => setActive((prevState) => !prevState)}
>
{answer}
</p>
);
};
You can use the above component in the map and display your answers.
Attached is a sandbox for reference.

How to pass state from a button to another component

When I click on a button, I am adding a product to the cart but I also want to set the state of a side drawer to true so it appears. This is working but trying to pass that state to the component so that when I click on close is giving me trouble. Here are basically all the moving parts:
const [isOpen, setIsOpen] = useState(false);
const addToCartHandler = async (id) => {
setIsOpen(true);
// add to cart logic here
}
<CartDrawer toggleOpen={isOpen} />
<Button
variant="primary"
onClick={() => addToCartHandler(id)}
>
Add to Cart
</Button>
This is working fine. I click on add to cart, it adds to cart and my modal shows up as expected.
The modal is basically component and I am receiving toggleOpen as props. Here is the CartDrawer component
const CartDrawer = (props) => {
const [isOpen, setIsOpen] = useState(false);
const closeNavHandler = () => {
setIsOpen(false);
};
return (
<div
id="mySidenav"
className={props.toggleOpen ? "sidenav open" : "sidenav"}
>
<a
className="closebtn"
onClick={closeNavHandler}
>
×
</a>
</div>
);
};
export default CartDrawer;
I know this is wrong but I can't figure out how to update the state here correctly to close it.
Just control everything from the parent. The cartDrawer only needs to receive a ìsOpen prop to know its state. Don't write another state in it.
A component like this should be stupid. It receives informations, and display them. Don't spill the logic all over your components. Just have a single source of truth.
// Main
const [isOpen, setIsOpen] = useState(false);
const addToCartHandler = async (id) => {
setIsOpen(true);
// add to cart logic here
}
<CartDrawer isOpen={isOpen} onClose={()=> setIsOpen(false)}/>
<Button
variant="primary"
onClick={() => addToCartHandler(id)}
>
Add to Cart
</Button>
// CartDrawer
const CartDrawer = ({isOpen, onClose}) => {
return (
<div
id="mySidenav"
className={isOpen ? "sidenav open" : "sidenav"}
>
<a
className="closebtn"
onClick={onClose}
>
×
</a>
</div>
);
};
export default CartDrawer;
I think that's what you want?
const [isOpen, setIsOpen] = useState(false);
const addToCartHandler = async (id) => {
setIsOpen(true);
// add to cart logic here
}
const toggleOpenDrawer = (val) => {
setIsOpen(val);
}
<CartDrawer toggleOpenDrawer={toggleOpenDrawer} toggleOpen={isOpen} />
<Button
variant="primary"
onClick={() => addToCartHandler(id)}
>
Add to Cart
</Button>
const CartDrawer = (props) => {
const { toggleOpenDrawer } = props
return (
<div
id="mySidenav"
className={props.toggleOpen ? "sidenav open" : "sidenav"}
>
<a
className="closebtn"
onClick={toggleOpenDrawer(!props.toggleOpen)}
>
×
</a>
</div>
);
};
export default CartDrawer;

setting states in mapped elements react

I'm having a problem setting the states of other mapped elements to false when clicking on one of the individual mapped element. For example,
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
{edit?<i className='fal fa-times'></i>:''}
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
The issue is that when you click on one of the icons it will set the state to true, but then if you click on another element's icon, the previously clicked element's state will remain true. I want to be able to set the previously clicked element's state to false when the user clicks on another element's icon.
EDIT: here is a video further explaining what I mean
https://gyazo.com/d8123c9f9a5fcfc48b2149c7faf48bad
Maybe try this, but I am not sure if this wont cause current icon to close down.
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
return <div>
<i
className='fal fa-edit'
onClick={(e)=>{
setEdit(true);
e.stopPropagation()
}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
if(edit){
setEdit(false)
}
},[edit])
Or try something like
const [edit, setEdit] = useState(false)
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>{
const checkEl = (e, currentEl) => {
if(el === currentEl){
e.stopPropagation()
setEdit(true)
}else{
setEdit(false)
}
}
return <div>
<i
className='fal fa-edit'
onClick={(e) => checkEl(e, el)}
></i>
<span>{el}</span>
</div>
})
useEffect(()=>{
document.addEventListener('click', ()=>{setEdit(false)})
},[])
Something like that.
FULL CODE SOLUTION
const MainComponent = () => {
const [currentEdit, setCurrentEdit] = useState('')
const array = ['witch-king', 'sauron', 'azog']
const arrayrow = array && array.map(el=>(
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
)
return (<> {arrayrow} Some JSX </>)
}
const ChildComponent = ({ el, handleClick, currentEdit }) => {
return (
<div>
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
<span>{el}</span>
</div>
)
}
EXPLANATION
Instead of using a boolean, I used the value of each item to represent the currently edited item, and initially, it is set to an empty string like this
const [currentEdit, setCurrentEdit] = useState('')
I made a child component passing each the currentEdit and the setCurrentEdit as props.
<ChildComponent
key={el}
el={el}
currentEdit={currentEdit}
handleClick={setCurrentEdit}>
)
Then do a check inside the child component and reset the currentEdit to that element on click
<i
className={`fal ${(currentEdit === el) ? 'fa-times' : 'fa-edit' }`}
onClick={(e) => {
handleClick(el)
}
></i>
PS: Remove the useEffect with the event listener, adding event listeners like that is an anti-pattern in React, and also you don't need it with this solution.

Using React Hook to search and filter items from Api with pagination

Using react hooks, I'm making a call to an api and displaying items in the app component calling a book and pagination functional component.
I have a search component placed at the top in the App return. Can anyone please help:
When the search button is clicked after inserting a book name, then books with similar names should be displayed
const SearchBooks =() => {
return (
<InputGroup>
<FormControl
type="text"
placeholder="Search books"
onChange={e => (e.target.value)}
/>
<InputGroup.Append>
<Button >
Search
</Button>
</InputGroup.Append>
</InputGroup>
);
}
const Book = ({books, loading}) => {
if(loading) {
return <h2>Loading...</h2>
}
return (books.map((book) =>
<ListGroup className="text-primary" key={book.id}>
<ListGroup.Item>
<h4>{book.book_title}</h4>
<li>Author : {book.book_author}</li>
<li>Publication Year : {book.book_publication_year}</li>
<li>Publication Country : {book.book_publication_country}</li>
<li>Publication City : {book.book_publication_city}</li>
<li >Pages : {book.book_pages}</li>
</ListGroup.Item>
</ListGroup>
));
}
const App = () => {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [booksPerPage] = useState(2);
const [search, setSearch] = useState('');
useEffect(() => {
const fetchBooks = async () => {
setLoading(true);
const res = await axios.post("http://nyx.vima.ekt.gr:3000/api/books");
setBooks(res.data.books);
setLoading(false);
};
fetchBooks();
}, []);
// Get current books
const indexOfLastBook = currentPage * booksPerPage;
const indexOfFirstBook = indexOfLastBook - booksPerPage;
const currentPosts = books.slice(indexOfFirstBook, indexOfLastBook);
// Change page
const paginate = pageNumber => setCurrentPage(pageNumber);
return (
<div className='container mt-5'>
<SearchBook/>
<Book books={currentPosts} loading={loading}/>
<Pagination
booksPerPage={booksPerPage}
totalBooks={books.length}
paginate={paginate}
/>
</div>
);
}
import React from 'react';
const Pagination = ({ booksPerPage, totalBooks, paginate }) => {
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(totalBooks / booksPerPage); i++) {
pageNumbers.push(i);
}
return (
<nav className="justify-content-center">
<ul className='pagination'>
{pageNumbers.map(number => (
<li key={number} className='page-item'>
<a onClick={() => paginate(number)} href='!#' className='page-link'>
{number}
</a>
</li>
))}
</ul>
</nav>
);
};
const currentPosts = books.fliter(book => book.title.includes(keyword)).slice(indexOfFirstBook, indexOfLastBook);
and you should recalculate the pagination and reset page number too, so ppl can still navigate to pages if the search result is too long.
you can also use useMemo hooks to optimize it, so it wont filter the array again on every re-render.
const currentPosts = useMemo(() => books.filter(...).slice(...), [books, keyword]);

Resources