Menu won't open on click (ReactJS) - reactjs

I'm trying to close the open menu once an outside click is triggered. I managed to implement it on a single button but now as I want to map a list of buttons, I can't manage to open any of the button menus.
import "./navbar.css";
import React, { useEffect, useRef, useState } from "react";
import { MenuItems } from "./menuItems";
export const Navbar = () => {
const [toggle, setToggle] = useState(true);
const btnRef = useRef();
const handleClick = () => {
setToggle(!toggle);
};
useEffect(() => {
const closeDropdown = (e) => {
if (e.path[0] !== btnRef.current) {
setToggle(false);
}
};
document.body.addEventListener("click", closeDropdown);
return () => {
document.body.removeEventListener("click", closeDropdown);
};
}, []);
return (
<div>
<div className="menu1">
<i className="fa fa-home" id="home"></i>
{MenuItems.map((n, i) => (
<li key={i} className="list">
<button ref={btnRef} onClick={handleClick} className="btn1">
{n.name}
<i className="fa fa-caret-down"></i>
</button>
</li>
))}
</div>
<div className={toggle ? "d-active" : "d-inactive"}>
<div className="dropdown">Empty</div>
</div>
</div>
);
};

My approach to this would be to have an OutsideDetector wrapper, with outsideClickHandler.
OutsideClickWatcher.js
const OutsideClickWatcher = (props) => {
const { onClickOutside } = props;
const wrapperRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
onClickOutside();
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [wrapperRef, onClickOutside]);
return <div ref={wrapperRef}>{props.children}</div>;
};
Navbar.js
export const Navbar = () => {
const [toggle, setToggle] = useState(true);
const btnRef = useRef();
const handleClick = () => {
setToggle(!toggle);
};
return (
<div>
<div className="menu1">
<i className="fa fa-home" id="home"></i>
{MenuItems.map((n, i) => (
<OutsideClickWatcher key={i} onClickOutside={() => setToggle(false)}>
<li className="list">
<button ref={btnRef} onClick={handleClick} className="btn1">
{n.name}
<i className="fa fa-caret-down"></i>
</button>
</li>
</OutsideClickWatcher>
))}
</div>
<div className={toggle ? "d-active" : "d-inactive"}>
<div className="dropdown">Empty</div>
</div>
</div>
);
};
Why do I prefer this approach? It gives me the flexibility to pass custom outside click handlers, and it's very reusable for all sorts of ui elements.
Note that this is a boilerplate code, it can obviously be improved further.

Related

How can i stop my array from shuffling onClick

So i'm building a Quiz App using Next Js and everything was going fine until i find out my array keeps Re-shuffling onClick of any button and i'm stumped on how to stop it.....I have tried adding e.preventDefault on all the button functions but nothing is working, So anytime i click an option it reshuffles the array thereby causing newArray[0] to always get the wrong answers, Any ideas on how i can stop reshuffling onClick??, Thanks in Advance
import axios from "axios";
import Axios from "axios";
import React, { useContext, useEffect, useState } from "react";
import styles from "../../styles/Questions.module.css";
import Login from "../Login";
import arrayShuffle from "array-shuffle";
import { AppContext } from "../helpers/helpers";
import Link from "next/link";
import Solutions from "../solutions";
export default function quiz({ questions }) {
const link = `https://the-trivia-api.com/api/questions`;
const [number, setNumber] = useState(0);
const [quizData, setQuizData] = useState(questions);
const [state, setState] = useState();
const [image, setImage] = useState();
const [numbering, setNumbering] = useState(1);
const [initialRenderComplete, setInitialRenderComplete] = useState(false);
const [chosenOption, setChosenOption] = useState("");
const [showAnswer, setShowAnswer] = useState(false);
const { score, setScore } = useContext(AppContext);
useEffect(() => {
if (localStorage) {
const getNameState = localStorage.getItem("name");
setState(getNameState);
} else {
console.log("errr");
}
}, []);
useEffect(() => {
if (localStorage) {
const getPhotoState = localStorage.getItem("photoUrl");
setImage(getPhotoState);
} else {
console.log("err");
}
}, []);
// Assigning incorrect answer array to oldArray
const oldArray = [
...quizData[number].incorrectAnswers,
quizData[number].correctAnswer,
];
const showAns = (e) => {
e.preventDefault();
setShowAnswer(!showAnswer);
};
// Pushing the correct answer into the incorrect answer array and forming a new array
oldArray.push(quizData[number].correctAnswer);
const newArray = [...new Set(oldArray)];
// Increase the number & index onClick of Next button
const increase = (e) => {
e.preventDefault();
setNumber(number + 1);
setNumbering(numbering + 1);
// checking if correct Answer is in the array then assigning it to getAns
const getAns = newArray.find(
(element) => element === quizData[number].correctAnswer
);
// If the getAns == Whatever You chose increase the score
if (getAns == chosenOption) {
setScore(score + 1);
}
};
// Only for the Finish Quiz Button
const finish = () => {
const getAns = newArray.find(
(element) => element === quizData[number].correctAnswer
);
if (getAns == chosenOption) {
setScore(score + 1);
}
};
const decrease = () => {
setNumber(number - 1);
setNumbering(numbering - 1);
};
// UseEffect to check against Hydration - Important!
useEffect(() => {
setInitialRenderComplete(true);
}, []);
const array = (e) => {
e.preventDefault();
setChosenOption(newArray[1]);
};
if (!initialRenderComplete) {
return null;
} else {
newArray.sort(() => (Math.random() > 0.5 ? 1 : -1)); // Shuffling the New array we pushed the
return (
<div className={styles.container}>
<div className={styles.profile}>
<div className={styles.mainProfile}>
<div className={styles.profilePic}>
<img src={image} alt="img" />
</div>
<h2>{state} </h2>
</div>
</div>
<div className={styles.main}>
<h2>
{numbering}. {questions[number].question}{" "}
</h2>
<div className={styles.list}>
<ul className={styles.list1}>
<div className={styles.flex}>
<h3>A. </h3>
<button onClick={array}>
<li>{newArray[1]} </li>
</button>
</div>
<div className={styles.flex}>
<h3>B. </h3>
<button onClick={() => setChosenOption(newArray[2])}>
{" "}
<li> {newArray[0]} </li>
</button>
</div>
</ul>
<ul className={styles.list2}>
<div className={styles.flexOption}>
<h2>C. </h2>
<button onClick={() => setChosenOption(newArray[0])}>
<li>{newArray[3]}</li>
</button>
</div>
<div className={styles.flexOption}>
<h2>D. </h2>
<button onClick={() => setChosenOption(newArray[3])}>
<li>{newArray[2]} </li>
</button>
</div>
</ul>
</div>
<div className={styles.btnStyle}>
<button onClick={decrease} className={styles.prev}>
Previous{" "}
</button>
{numbering == quizData.length ? (
<Link href="./scores">
<button className={styles.next} onClick={finish}>
{" "}
Finish Quiz{" "}
</button>
</Link>
) : (
<button onClick={increase} className={styles.next}>
Next{" "}
</button>
)}
{showAnswer ? (
<button onClick={showAns} className={styles.next}>
Hide Answer{" "}
</button>
) : (
<button onClick={showAns} className={styles.next}>
See Answer{" "}
</button>
)}
</div>
<p> {showAnswer ? quizData[number].correctAnswer : ""}</p>
</div>
</div>
);
}
}
export const getStaticProps = async () => {
const data = await Axios.get("https://the-trivia-api.com/api/questions");
// const data = req;
const initialData = data.data;
return {
props: {
questions: initialData,
},
};
};
And anytime i console.log the answer i pick, i don't get that option i clicked, So how can i extract the option i clicked on?
const arrayZero = (e) => {
e.preventDefault();
setChosenOption(newArray[0]);
};
const arrayOne = (e) => {
e.preventDefault();
setChosenOption(newArray[1]);
};
return(
//arrayZero being the function
<button onClick={arrayZero}>
<li>{newArray[0]} </li>
</button>
)
e.preventDefault() actually worked, Though it did not stop my array from reshuffling when i click on a button but it made my "chosenOption" state stay the same without changing.

Update one element of big list without re render others elements in react hooks?

i want to optimize my react App by testing with a large list of li
Its a simple todo List.
By exemple, when click on a li, task will be line-through, and check icon will be green. This simple action is very slow with a large list because, the whole list is re render.
How to do this with React Hooks?
function App() {
const [list, setList] = useState([]);
const [input, setInput] = useState("");
const inputRef = useRef(null);
useEffect(() => inputRef.current.focus(), []);
//Pseudo Big List
useEffect(() => {
const test = [];
let done = false;
for (let i = 0; i < 5000; i++) {
test.push({ task: i, done });
done = !done;
}
setList(test);
}, []);
const handlerSubmit = (e) => {
e.preventDefault();
const newTask = { task: input, done: false };
const copy = [...list, newTask];
setList(copy);
setInput("");
};
const checkHandler = (e, index) => {
e.stopPropagation();
const copy = [...list];
copy[index].done = !copy[index].done;
setList(copy);
};
const suppression = (e, index) => {
e.stopPropagation();
const copy = [...list];
copy.splice(index, 1);
setList(copy);
};
const DisplayList = () => {
return (
<ul>
{list.map((task, index) => (
<Li
key={index}
task={task}
index={index}
suppression={suppression}
checkHandler={checkHandler}
/>
))}
</ul>
);
};
//JSX
return (
<div className='App'>
<h1>TODO JS-REACT</h1>
<form id='form' onSubmit={handlerSubmit}>
<input
type='text'
placeholder='Add task'
required
onChange={(e) => setInput(e.target.value)}
value={input}
ref={inputRef}
/>
<button type='submit'>
<i className='fas fa-plus'></i>
</button>
</form>
{list.length === 0 && <div id='noTask'>No tasks...</div>}
<DisplayList />
</div>
);
}
export default App;
Li component
import React from "react";
export default function Li(props) {
return (
<li
onClick={(e) => props.checkHandler(e, props.index)}
className={props.task.done ? "line-through" : undefined}
>
{props.task.task}
<span className='actions'>
<i className={`fas fa-check-circle ${props.task.done && "green"}`}></i>
<i
className='fas fa-times'
onClick={(e) => props.suppression(e, props.index)}
></i>
</span>
</li>
);
}
CodeSandbox here: https://codesandbox.io/s/sad-babbage-kp3md?file=/src/App.js
I had the same question, as #Dvir Hazout answered, I followed this article and made your code the changes you need:
function App() {
const [list, setList] = useState([]);
const { register, handleSubmit, reset } = useForm();
//Pseudo Big List
useEffect(() => {
const arr = [];
let done = false;
for (let i = 0; i < 20; i++) {
arr.push({ id: uuidv4(), task: randomWords(), done });
done = !done;
}
setList(arr);
}, []);
const submit = ({ inputTask }) => {
const newTask = { task: inputTask, done: false, id: uuidv4() };
setList([newTask, ...list]);
reset(); //clear input
};
const checkHandler = useCallback((id) => {
setList((list) =>
list.map((li) => (li.id !== id ? li : { ...li, done: !li.done }))
);
}, []);
const suppression = useCallback((id) => {
setList((list) => list.filter((li) => li.id !== id));
}, []);
//JSX
return (
<div className="App">
<h1>TODO JS-REACT</h1>
<form onSubmit={handleSubmit(submit)}>
<input type="text" {...register("inputTask", { required: true })} />
<button type="submit">
<i className="fas fa-plus"></i>
</button>
</form>
{list.length === 0 && <div id="noTask">No tasks...</div>}
<ul>
{list.map((task, index) => (
<Li
key={task.id}
task={task}
suppression={suppression}
checkHandler={checkHandler}
/>
))}
</ul>
</div>
);
}
Li component
import React, { memo } from "react";
const Li = memo(({ task, suppression, checkHandler }) => {
// console.log each time a Li component re-rendered
console.log(`li ${task.id} rendered.`);
return (
<li
onClick={(e) => checkHandler(task.id)}
className={task.done ? "line-through" : undefined}
>
{task.task}
<span className="actions">
<i className={`fas fa-check-circle ${task.done && "green"}`}></i>
<i className="fas fa-times" onClick={(e) => suppression(task.id)}></i>
</span>
</li>
);
});
export default Li;
You can check it live here
I know it's probably late for your question, but may help others ;)
You can use React.memo and wrap the Li component. This will cache the instances of the Li component based on shallow comparison. Read more in the docs
Otherwise, if you don't need the state in the container, you can keep it locally in the Li component and then it won't cause a whole list rerender.

Dynamic Dropdown Menu Next.js, Strapi

I'm using Strapi and Next.js to create a Dropdown Menu, but when i hover on a menu button, all dropdown container of the menu show up.
Can anybody show me the way to do it right? Thank you very much!
This is my code:
const Navbar = ({ navbar }) => {
const [open, setOpen] = useState(false);
const [mobileMenuIsShown, setMobileMenuIsShown] = useState(false);
const handleClick = () => {setOpen(!open)};
const onMouseEnter = () =>{
if (window.innerWidth < 960) {
setOpen(false);
} else {
setOpen(true);
}
};
const onMouseLeave = () =>{
if (window.innerWidth < 960){
setOpen(false);
} else {
setOpen(false);
}
};
return (
<>
<nav>
<div>
{navbar.topmenu.map((navMenu) => (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
key={navMenu.id}
>
<button
onClick={handleClick}>
<span class="mr-1">{navMenu.label}</span>
</button>
{open &&
<ul>
{navMenu.submenu.map((link) => (
<li key={link.id}>
<CustomLink link={link}>{link.text}</CustomLink>
</li>
))}
</ul>
}
</div>
))}
</div>
</nav>
</>

React: How to update state for just one element, rather than batch update

I am a beginner with React. I have a project I'm working on with some sample travel tours. I would like to use a "read more/show less" feature for the description of each tour. The read more/show less button is toggling, but it's showing more or less description for all of the tours when clicked, when I want it to just toggle the tour that's clicked. In other words, it's updating the state for ALL tours, rather than just the one that's clicked. Hopefully that makes sense. Please help! Thanks in advance.
import React, { useState, useEffect } from 'react';
import './index.css';
const url = 'https://course-api.com/react-tours-project';
const Tour = () => {
const [tourItem, setTourItem] = useState('');
const removeItem = (id) => {
let newList = tourItems.filter((item) => item.id !== id);
setTourItem(newList);
};
const [fetchingData, setFetchingData] = useState(true);
useEffect(() => {
const abortController = new AbortController();
const fetchUrl = async () => {
try {
const response = await fetch(url, {
signal: abortController.signal,
});
if (fetchingData) {
const data = await response.json();
setTourItem(data);
}
setFetchingData(false);
} catch (e) {
console.log(e);
}
};
fetchUrl();
return () => {
//cleanup!
abortController.abort();
};
});
const tourItems = Object.values(tourItem);
const [readMore, setReadMore] = useState(false);
return (
<>
{tourItems.map((item) => {
return (
<div key={item.id}>
<article className='single-tour'>
<img src={item.image} alt={item.name} />
<footer>
<div className='tour-info'>
<h4>{item.name}</h4>
<h4 className='tour-price'>
${item.price}
</h4>
</div>
{readMore ? (
<p>
{item.info}
<button
onClick={() => setReadMore(false)}
>
Show Less
</button>
</p>
) : (
<p>
{item.info.slice(0, 450) + '...'}
<button
onClick={() => setReadMore(true)}
>
Read More
</button>
</p>
)}
<button
className='delete-btn'
onClick={() => removeItem(item.id)}
>
Not Interested
</button>
</footer>
</article>
</div>
);
})}
</>
);
};
export default Tour;
Good question! It happened because you share the readMore state with all of the tour items. You can fix this by encapsulating the tour items into a component.
It should look something like this;
The component that encapsulates each tour items
import React, {useState} from "react";
import "./index.css";
const SpecificTourItems = ({item, removeItem}) => {
const [readMore, setReadMore] = useState(false);
return (
<div key={item.id}>
<article className="single-tour">
<img src={item.image} alt={item.name} />
<footer>
<div className="tour-info">
<h4>{item.name}</h4>
<h4 className="tour-price">${item.price}</h4>
</div>
{readMore ? (
<p>
{item.info}
<button onClick={() => setReadMore(false)}>Show Less</button>
</p>
) : (
<p>
{item.info.slice(0, 450) + "..."}
<button onClick={() => setReadMore(true)}>Read More</button>
</p>
)}
<button className="delete-btn" onClick={() => removeItem(item.id)}>
Not Interested
</button>
</footer>
</article>
</div>
);
};
export default SpecificTourItems;
the component that fetch & maps all the tour items (your old component :))
import React, {useState, useEffect} from "react";
import SpecificTourItems from "./SpecificTourItems";
const url = "https://course-api.com/react-tours-project";
const Tour = () => {
const [tourItem, setTourItem] = useState("");
const removeItem = (id) => {
let newList = tourItems.filter((item) => item.id !== id);
setTourItem(newList);
};
const [fetchingData, setFetchingData] = useState(true);
useEffect(() => {
const abortController = new AbortController();
const fetchUrl = async () => {
try {
const response = await fetch(url, {
signal: abortController.signal,
});
if (fetchingData) {
const data = await response.json();
setTourItem(data);
}
setFetchingData(false);
} catch (e) {
console.log(e);
}
};
fetchUrl();
return () => {
//cleanup!
abortController.abort();
};
});
const tourItems = Object.values(tourItem);
const [readMore, setReadMore] = useState(false);
return (
<>
{tourItems.map((item, key) => {
return (
<SpecificTourItems item={item} removeItem={removeItem} key={key} />
);
})}
</>
);
};
export default Tour;
I hope it helps, this is my first time answering question in Stack Overflow. Thanks & Good luck!

React Router: clicking on mapped items to a new page

I'm working on an ecommerce site. Currently, I have a page which maps over all of the items from the API and displays them on screen.
I'm trying to make it so that when one of the mapped items is clicked, the user will be taken to a new page ("Item") featuring just that item. I'm using React Router, but it's not working.
Any advice to get me in the right direction on how to implement this would be appreciated.
Please specifically see the return statement and how I added the Link routers.
import React, { useState, useEffect } from 'react';
import './../App.css';
import * as ReactBootStrap from 'react-bootstrap';
import {Link} from 'react-router-dom';
function Shop() {
const [products, setProducts] = useState([]);
const [filterProducts, setFilteredProducts] = useState([]);
const [item, setItem] = useState('');
const [currentSort, setCurrentSort] = useState('');
const [loading, setLoading] = useState(false);
useEffect(async () => {
fetchItems();
}, [])
const fetchItems = async () => {
const data = await fetch('https://fakestoreapi.com/products');
const items = await data.json();
setProducts(items)
setLoading(true)
}
function priceUSD(change){
return change.toFixed(2)
}
useEffect(() => {
const filteredItems = products.filter((a) => {
if (item === '') {return a} else {return a.category === item}
});
setFilteredProducts(filteredItems);
}, [item, products])
useEffect(() => {
if (currentSort === '') {
return
}
const sortedItems = filterProducts.sort((a, b) => {
return currentSort === 'ASE' ? a.price - b.price : b.price - a.price
});
setFilteredProducts([...sortedItems]);
}, [currentSort])
return (
<div>
<div className="itemSort">
<p onClick={() => setItem("")}>All items</p>
<p onClick={() => setItem("men clothing")}>Men clothing</p>
<p onClick={() => setItem("women clothing")}>Women clothing</p>
<p onClick={() => setItem("jewelery")}>Jewelery</p>
<p onClick={() => setItem("electronics")}>Electronics</p>
</div>
<div className="itemSort">
<p>Order by price</p>
<p onClick={() => setCurrentSort('DESC')}>Highest</p>
<p onClick={() => setCurrentSort('ASE')}>Lowest</p>
</div>
<div className="gridContainer">
{loading ? <Link to="/Item">
(filterProducts.map((a, index) => (
<div key={index} className="productStyle">
<img src={a.image} className="productImage"></img>
<p>{a.title}</p>
<p>${priceUSD(a.price)}</p>
</div>
))) </Link> : (<ReactBootStrap.Spinner className="spinner" animation="border" />)
}
</div>
</div>
)
}
export default Shop;
You need a route like /item/:id to have one page for one item and assuming that a product has an id:
<div className="gridContainer">
{loading ?
(filterProducts.map((a, index) => (
<Link to={`/Item/${a.id}`}>
<div key={index} className="productStyle">
<img src={a.image} className="productImage"></img>
<p>{a.title}</p>
<p>${priceUSD(a.price)}</p>
</div>
</Link>
:
(<ReactBootStrap.Spinner className="spinner" animation="border" />)
}
</div>

Resources