I am trying to give an active className to mapped data when I click. Currently the previous active class doesn't change when i click on another text - reactjs

Parent component
Here is the mapped function
function SubHeader() {
const categories = category?.data?.data;
return (
{categories?.map((data) => (
<Smaller data={data} />
))} );
}
Child component
Here is where I am using the state to control the color of the text when it is clicked. Not sure I can figure what isn't right.
function Smaller({ data }) {
const [active, setActive] = useState(false);
const colorPicker = (dataId) => {
setActive(dataId ? !active : active);
};
return (
<Text
color={active ? 'brand.blue' : 'brand.dark'}
onClick={() => colorPicker(data?.id)}
>
{data?.name}
</Text>
);
}

The issue is when you click on another text, it doesn't change the active state of other / previous texts. So you're just toggling the active class of each component but it doesn't "untoggle" anywhere.
I found a way to solve this but I used a simple example because I didn't have your data.
Solution:
Each child component should have an ID.
Check if the child component's ID matches the activeElementID.
Parent Component
function SubHeader() {
const [activeElementID, setActiveElementID] = useState()
const categories = category?.data?.data;
return (
{categories?.map((data) => {
<Smaller data = {data} id = {data?.id} activeElementID={activeElementID} setActiveElementID={setActiveElementID} />
})}
)
}
Child Component
function Smaller({data, id, activeElementID, setActiveElementID}) {
function clicked() {
setActiveElementID(id)
}
return (
<p onClick={clicked} className={activeElementID === id ? "blue" : "red"}>{data}</p>
)
}
Furthermore, I would also recommend checking the data instead of using the "?" operation. For example, category?.data and data?.data
Because you are saying that you are sure the data exists. You can do this instead.
const categories = category.data.data
if (categories) {
return (....)
}
Hope this helps!

Related

Prevent useContext value from updating every instance of a mapped component

I have a long list of components which takes a value from my Context.
Component has a unique id, and also knows which is the "current" id. I only want the Component whose id matches the currentId to update when the value from myContext changes.
const ComponentList = () => {
return(
<>
{data.map((d,i) =>
<Component key={i} id={d.id} />
}
</>
)
}
const Component = ({id}) => {
const {value} = useContext(MyContext);
const {currentId} = useContext(OtherContext);
//only re-render if `id === currentId` and `value` changes
return (
<h1>{value}</h1>
)
}
Note that the value from myContext does not care about which Component id is the current id.
I figure the answer lies somewhere in memoization but I haven't been successful in finding it. (I was able to get this working as I intend by doing const {value} = id === currentId ? useContext(MyContext) : {value: null} but this is obviously not okay)
I've abstracted this to a very generic example but if more context (haha) is necessary I can explain my use case further.
Seems like React.memo was the answer and it was fairly simple; just didn't see it at first.
I wrapped the Component in a memo and added my own condition.
const ParentComponent = (props) => {
const {value} = useContext(MyContext);
const {currentId} = useContext(OtherContext);
return (
<Component id={props.id} value={value} currentId={currentId} />
)
}
const Component = memo(
({value}) => {
return (
<h1>{value}</h1>
)
},
(prev, next) =>
prev.value === next.value || prev.id !== next.currentId
);
This essentially will not update if the previous and next value are equal, or if the id does not match the currentId.

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

React custom hook with state variable as parameter

I have a react function component which sets an array of ids based on an user event on a link click(which opens a popup with some options that can be selected and has a callback once it is closed which will return the id of the selected element). these ids are passed to a child component which has a custom hook which uses these ids to perform some action. whenever i click on the link and select an element and close the popup.. get the error
"VM10715 react_devtools_backend.js:2430 You have changed a parameter while calling a hook which is supposed to remain unchanged [Array(2)]
0: (2) ["", "asdsadsad"]
lastIndex: (...)
lastItem: (...)
length: 1"
is there a way to make this work without running into this error? please see the code sample below
const TestComp = () => {
const [newIds, setNewIds] = useState([]);
const onPopupElementSelect = (ids) => {
setNewIds([...newIds, ids]);
};
return (
//renders some components
<>
<ImageComponent images={images} ids={newIds} onClick={handleClick} />
<Popup onSelect={onPopupElementSelect} />
</>
);
};
const ImageComponent = (props) => {
const { newIds, images } = props;
const newImages = useImages(ids || ['']); //customhook that fetches image details by ids
const imgs = images.map((i) => (
<div key={i.imageId}>
<img src={i.imageUrl} alt="" />
<Link onClick={handleClick} /> //opens the popup for user to select a new
image
</div>
));
return <div>{imgs}</div>;
};
ps: the paramerter names are not the issue.. this code is just a sample to give the basic idea of what i'm trying to do.
I think it is because you gave the same name to parameter and the state may be try newID as the parameter name
const onPopupElementSelect = (newId) => {
setIds(oldIds => [...oldIds, newId]);
};

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

How to target a specific item to toggleClick on using React Hooks?

I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.

Resources