i just started with React and Next.js and I'm messing around with hooks and i got stuck, i created a vertical menu, i want to display the content below each title, but only one at a time taking in mind these 2 options:
if i click a title which content is already displayed, the content will just hide.
if i click a different title, the content of the last title will hide and the content of the new title will appear below it.
Thank you so much for your time.
I made a sandbox of the basic logic of my code:
https://codesandbox.io/s/affectionate-morning-tyj8j?file=/src/App.tsx
That same code is this:
import {useState} from "react";
export default function Test() {
const [content1, setContent1] = useState(null);
const [content2, setContent2] = useState(null);
const [content3, setContent3] = useState(null);
return (
<div>
<div onClick={() => setContent1(Content_1)}>
Click to show content 1
<div>
{content1}
</div>
</div>
<div onClick={() => setContent2(Content_2)}>
Click to show content 2
<div>
{content2}
</div>
</div>
<div onClick={() => setContent3(Content_3)}>
Click to show content 3
<div>
{content3}
</div>
</div>
</div>
)
}
const Content_1 = () => {
return (
<p>Content 1</p>
)
}
const Content_2 = () => {
return (
<p>Content 2</p>
)
}
const Content_3 = () => {
return (
<p>Content 3</p>
)
}
You should take a step back and think a bit more abstractly about what your UI interaction means.
if i click a title which content is already displayed, the content will just hide.
if i click a different title, the content of the last title will hide and the content of the new title will appear below it.
So effectively you only ever have one content section open/visible at-a-time. There is no need to store individual states for each content section you want to manually toggle, and as the number of content sections grows you'll need to increase number of toggles linearly. This is a terribly inefficient way to scale.
I suggest using a single state to hold which content section is active. Use a toggle handler to either toggle back "closed" a section if its id is set again, otherwise set a new section id. Conditionally render the section if its id matches the active id set in state.
function App() {
const [active, setActive] = useState<number>(-1);
const toggleHandler = (id: number) => () =>
setActive((active) => (active === id ? -1 : id));
return (
<div>
<div onClick={toggleHandler(1)}>
Click to show content 1{active === 1 && <Content1 />}
</div>
<div onClick={toggleHandler(2)}>
Click to show content 2{active === 2 && <Content2 />}
</div>
<div onClick={toggleHandler(3)}>
Click to show content 3{active === 3 && <Content2 />}
</div>
</div>
);
}
I used the same structure to be easier for you to understand :)
If you still have difficulties, keep me in touch.
Code suggested
import React, { useState } from 'react';
export default function Test() {
// states
const [content1, setContent1] = useState(true);
const [content2, setContent2] = useState(false);
const [content3, setContent3] = useState(false);
// content
const Content_1 = <p>Content 1</p>;
const Content_2 = <p>Content 2</p>;
const Content_3 = <p>Content 3</p>;
function setContent(id) {
setContent1(id === 1);
setContent2(id === 2);
setContent3(id === 3);
}
return (
<div>
<button onClick={() => setContent(1)}>Show 1</button>
<button onClick={() => setContent(2)}>Show 2</button>
<button onClick={() => setContent(3)}>Show 3</button>
{content1 && <div>{Content_1}</div>}
{content2 && <div>{Content_2}</div>}
{content3 && <div>{Content_3}</div>}
</div>
);
}
Related
I'm trying to make an accordion, that opens up upon clicking the "+" icon. But when I click on a single element, all the other elements expand. Is there a way to just open one of the accordions? I've added the screenshot of the accordion and also added the code.
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
const [display, setDisplay] = useState(false);
const clickHandle = () => {
setDisplay(!display);
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{data.map((question) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
<button className="btnContainer" onClick={clickHandle}>
{display === false ? (
<AiFillPlusCircle size="2.4em" />
) : (
<AiFillMinusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{display === false ? (
<></>
) : (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Explanation
Basically you can't use a single variable to toggle all your elements. All the elements will act as a single element, so either all will open or all will close.
You need something that can be checked against each element. Now id is a potential candidate but it has it's drawbacks, since it's a list the best option is using the index of the element itself.
So you first change the display type from boolean (false) to integer type and default it to -1 (anything less than zero)
Then change your .map function from .map((question) =>... to .map((question, questionIndex) =>..., this will get you a variable questionIndex which holds the current question's index
You can use that (questionIndex) and the display variable to check against each other and display the appropriate states.
Benefits when compared to other answers
Since you are dealing with a list of items, it is always best to use the index of an element to toggle the element's display, This ensures you have decoupled your View from your Data. (As much as possible)
If for some reason your id is null or duplicate, it will create issues in your display.
It is easier to just call toggleElement(2) to automatically open an element for a given position via code (on first load). This is useful if you want to maintain the open states between url changes / reloads, you just add the index to the query parameter of the url.
Solution
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
// Update the display to be of type integer and init it with -1
const [display, setDisplay] = useState(-1);
// Add a parameter to the click function to take the clicked element's index
const toggleElement = (currentIndex) => {
// Check if the element that is clicked is already open
if(currentIndex===display) {
setDisplay(-1); // If it is already open, close it.
}
else {
setDisplay(currentIndex); // else open the clicked element
}
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{/* Add a variable questionIndex to the map method to get the index of the current question */}
{data.map((question, questionIndex) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
{/* Update the button onClick event to pass the current element's index via the questionIndex variable */}
<button className="btnContainer" onClick={()=> toggleElement(questionIndex)}>
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<AiFillMinusCircle size="2.4em" />
) : (
<AiFillPlusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
) : (
<></>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Instead of a boolean, use an id in your state
const [display, setDisplay] = useState("");
When you map an item, add this function
const { id, title, info } = question;
const handleSetDisplay = () => {
if(id === display) {
//Close panel
setDisplay("")
} else {
//open specific panel
setDisplay(id)
}
}
Adjust the button's onClick
<button className="btnContainer" onClick={handleSetDisplay}>
Then to compare if your panel should expand, use
{display === id ? ///...: ...}
In short, you need to compare the saved ID with the mapped item's id.
If your id is a number, just change the initial state to 0
I'm new with React and Next.js, I'm creating a timeline, the timeline has colored bars which if pressed, the page shows a div below with info about each bar.
I managed to make the content below appear and disappear with useState hook so the content of each bar doesn't stack, I'm using an animated tag "Section" and only the first time I press any bar, the content is animated, the rest appears statically, I'm wondering if I can use something like the useEffect hook to maybe re-render each content div so the animation appears every time you click each bar, also to erase the last loaded div so doesn't stack on browser memory, I hope I explained myself I started with React 2 days ago, and thank you for your time.
Example reduced code:
//useState hook
const [content, setContent] = useState(null);
//Timeline section
<div>
<Bar1 onClick={() => setContent(BarContent_1)}/>
<Bar2 onClick={() => setContent(BarContent_2)}/>
</div>
//Content display
<div>
{content}
</div>
//Content of each bar, (<Section> div has the animation)
const BarContent_1 = () => {
return (
<Section>
Content of bar 1
</Section>
)
}
const BarContent_2 = () => {
return (
<Section>
Content of bar 2
</Section>
)
}
you can use useState for that to toggle classList on element here is example below what I do, in your project I don't know what you do but I will come up with card example first as your default value set state to false in card component and when you will click the button toggle that boolean false to true
like that () => setState(!state) it will always set the value of opposite state
when you will change state component always re-renders and checks the state, and you will make like that on more info div, if that state is true show more-info active that means your div will be displayed
and if it is false it will be dissapeared
const Card1 = () => {
const [showMore, setShowMore] = useState(false)
return (
<div className="card">
<div>Daniel</div>
<button onClick={() => setShowMore(!showMore)}>Show More</button>
<div class={showMore ? "more-info active": "more-info"}>This is more info Daniel</div>
</div>
)
}
also here is my css what I do
.more-info{
opacity: 0;
transition: 0.5s ease;
}
.active{
opacity: 1
}
in start thats good to make stuff like that but I would recommend you to use array objects for that to .map() components and make toggling animations on that, good luck
also quickly I made working example on sandbox
#callmenikk gave me an idea to use a conditional to render my styled div every time the condition met.
Solution:
//useState hook
const [content, setContent] = useState(null);
//Conditional function 'Refresh'
const Refresh = ({children}) => {
const active = content != setContent
if (active) {
return (
<Section>
{children}
</Section>
)
}
}
//Timeline section
<div>
<Bar1 onClick={() => setContent(BarContent_1)}/>
<Bar2 onClick={() => setContent(BarContent_2)}/>
</div>
//Content display and wrapping the content in my Refresh function
<Refresh>
{content}
</Refresh>
//Content of each bar, no need to wrap content in my styled (Section) tag since my Refresh button will
const BarContent_1 = () => {
return (
<div>
Content of bar 1
</div>
)
}
const BarContent_2 = () => {
return (
<div>
Content of bar 2
</div>
)
}
I am using this below function (functional component with hooks) to add items in to the Dom.
I want to keep the fontawosome icon in the code, hidden and once the user clicks on a utility div, an icon must show in UI, after the icon shows, the user can add an item in to the movements div the problem is I have to double click all the time to add that mentioned item instead of clicking once!!
any idea what is the deal with double click ?
import React, {useState} from 'react'
import StabelItem from './StabelItem';
import { icon, library } from '#fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faPlusCircle} from '#fortawesome/free-solid-svg-icons';
library.add(faPlusCircle)
function addItems() {
// item component imported !!
let item = [<StabelItem />]
// Icon State !! local
const [icons, setIcon] = useState([]);
// Items State !! local
const [stabelItem, setStabelItem] = useState({items:[]});
// Show the icon so the use can add itmes +
const showIcon = ()=> {
// maipulate the state so the icon can show !!
setIcon(<FontAwesomeIcon onClick={() => {addStabelItem()}} icon={["fas", "plus-circle"]}/>)
}
// add Items !! when clicking on the icon this funtion is inside of showicon !!
const addStabelItem = () => {
setStabelItem({items: [...stabelItem.items, ...item]})
}
return (
<div className="utility" onClick={showIcon}>
<li className="box-item" >
<div className="flexrow" >
<div className='number norest'>Total</div>
<div className='number calc margin-left norest'>0</div>
</div>
<ul className="movements">
{/* itmes gets added!! when icon is getting clicked */}
{stabelItem.items}
{/* icons Show on demand */}
{icons}
</ul>
</li>
</div>
)
}
I think you should edit the function you pass to the icon like this:
setIcon(<FontAwesomeIcon onClick={addStabelItem} icon={["fas", "plus-circle"]}/>)
I had to change the structure making the icon on show, rather than making the state showing it, so ternary operator solved the issue.
function StabelBoxUtility() {
// item component
let item = [<StabelItem />]
// Icon State
const [iconsVisble, setIconVisble] = useState(false);
// Items State
const [stabelItem, setStabelItem] = useState({items:[]});
// show Icon
const showIcon = () => setIconVisble(true)
// add Items !! the icon must be on display
const addStabelItem = () => {
setStabelItem({items: [...stabelItem.items, ...item]})
}
return (
<div className="utility" onClick={showIcon}>
<li className="box-item" >
<div className="flexrow" >
<div className='number norest'>Total</div>
<div className='number calc margin-left norest'>0</div>
</div>
<ul className="movements" >
{stabelItem.items}
{iconsVisble ? <FontAwesomeIcon className="changable margin-left" onClick={addStabelItem} icon={["fas", "plus-circle"]} size="lg" />: null}
</ul>
</li>
</div>
)
}
I'm new to coding and react and i'm trying to update x4 divs so when the user clicks it will update the content inside. Currently i have the code updating however it updates all 4 div's instead of the specific div which is clicked.
I know this will be controlled by using event however slightly confused what exactly i need to include.
Any help in the correct direction would be greatly appreciated. Current code runs fine but content within all 4 divs update rather than the specific one which is clicked
const [isActive, setIsActive] = useState(false)
const toggleContent = e => {
e.preventDefault()
setIsActive(!isActive)
}
Example div i'm trying to update when user clicks (currently i have x4 others which are the same logic but different HTML content. Currently when any of the 4 buttons are clicked, all x4 divs content change however i only want it to change the specific div which contained the button that was clicked
{!isActive ?
<article>
<h1>BEFORE CLICK TEXT</h1>
<a onClick={toggleClass}>BUTTON</a>
</article>
:
<article>
<p>AFTER CLICK TEXT</p>
<a onClick={toggleClass}>Back</a>
</article>
}
maintain activeIndex instead of just active boolean. In render method, based on activeIndex and index decide the active attributes for div.
Try the snippet.
const content = ['abc', 'def', 'ghi', 'jkl'];
const Component = () => {
const [activeIndex, setActiveIndex] = React.useState(-1);
return (
<div>
{content.map((item, index) =>
<div key={item}
style={{color: activeIndex === index ? "red" : "black" }}
onClick={() => setActiveIndex(index)}> {item}
</div>)}
</div>
)
}
ReactDOM.render(<Component />, document.getElementById('app'));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="app"> </div>
When I click on the image I want that it is automatically scrolled to the component which is then displayed. I tried with anchor tags, but it's not working (I believe due to the fact that the component is hidden and at the same time when it is shown it should be scrolled to it ) , useRef - I get the error 'not defined' (I believe same reason as above).
Component is displyed onClick, but it does't scroll to the view-port of the user. Pls help, I'm out of the ideas :/
const WebContent = () => {
const [hidden, setHidden] = useState(false)
return (
<div>
<img onClick={() => setHidden(true)} src={first}/>
<div>
{hidden && <MyComponent/>}
</div>
</div>
)}
Your intuition is probably right that MyComponent is not yet mounted when you try to scroll to it. A simple way to do this would be to have MyComponent scroll itself into view when it mounts, if that's the behavior you're looking for.
const MyComponent = () => {
const ref = React.useRef(null);
useEffect(() => {
if (ref.current) ref.current.scrollIntoView();
}, [ref]);
return (
<div ref={ref}>
NOW YOU SEE ME
</div>
);
};
export default MyComponent;
One (hacky?) idea is add the ref to the surrounding div of the hidden content:
this.scrollHere = React.useRef(null);
...
return (
<div style={{ minHeight: 1 }} ref={this.scrollHere}>
{hidden && <div>My Hidden Component</div>}
</div>
)
Then you can run a function onClick, which sets hidden to true (which by the way is kinda irritating. Maybe just use "shown" as a quick improvement) and also lets the ref scrollIntoView:
const showAndScroll = () => {
setHidden(true);
this.scrollHere.current.scrollIntoView({
behavior: "smooth"
});
};
The minHeight has to be placed on the div since it is at height of 0 first and this messes with the scroll function (it scrolls below the hidden content).
See working example here.