I'm new to React. I'm attempting to add an onClick event to a div element that will remove a className from another element. These elements are part of a loop in a map. I am attempting to use the useRef hook for this. I specifically don't want to toggle classNames, I want to remove it, and that's it. Then add it back with another onclick event from another element. This requirement is specific to my application. My current code removes the className from the last element, not the one I am targeting. Thanks in advance for any help!
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen);
});
}, []);
const navRef = React.useRef(null);
const onRemoveClick = (e) => {
navRef.current.classList.remove("red");
};
return (
<main>
{kitchenItems.map((item, index) => (
<div key={index} className="item">
<div onClick={onRemoveClick}>
<h2>{item.name}</h2>
<p ref={navRef} className="red">
{item.text}
</p>
</div>
</div>
))}
</main>
);
}
Here it is in CodeSandbox:
https://codesandbox.io/s/lively-moon-d7mm3
Save the indicator of whether an item should have the class or not into the kitchenItems state. Remove the ref.
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen.map(item => ({ ...item, red: true })));
});
}, []);
const onRemoveClick = (i) => {
setkitchenItems(
kitchenItems.map((item, j) => j !== i ? item : ({ ...item, red: false }))
);
};
<div onClick={() => onRemoveClick(i)}>
<h2>{item.name}</h2>
<p className={item.red ? 'red' : ''}>
The way you are approaching this is the way it's done with jQuery, that you change the dom elements. In React you would instead update the state and let the component render. I have added a new property on your objects but you may have another list or some other logic if you wish.
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
import "./styles.css";
export default function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios.get("./data.json").then((res) => {
setkitchenItems(res.data.kitchen);
});
}, []);
const onRemoveClick = (index) => {
debugger;
const tmpItems = [...kitchenItems];
tmpItems[index].isRed = !tmpItems[index].isRed;
setkitchenItems(tmpItems);
};
return (
<main>
{kitchenItems.map((item, index) => (
<div key={index} className="item">
<div onClick={() => onRemoveClick(index)}>
<h2>{item.name}</h2>
<p className={item.isRed ? "red" : ""}>{item.text}</p>
</div>
</div>
))}
</main>
);
}
In CodeSandbox: https://codesandbox.io/s/keen-kirch-55whe?file=/src/App.js
Why not just use pureJS?
const onRemoveClick = (e) => {
var target = e.target;
if (target) {
if (target.classList && target.classList.contains("red")) {
target.classList.remove("red");
} else {
target.classList.add("red");
}
}
};
Related
I am trying to make a flashcard web app for language learning and/or rote learning. I have managed to show the first element of the array which contains the data that I'm fetching from the backend but I can't switch from the first element to the subsequent elements.
Here is my code in React:
// Decklist component that displays the flashcard
import { React, useEffect, useState, useContext } from "react";
import Card from "./Card";
import cardContext from "../store/cardContext";
const axios = require("axios");
export default function Decklist() {
//State for data fetched from db
const [data, setData] = useState([]);
//State for array element to be displayed from the "data" state
const [position, setPosition] = useState(0);
//function to change the array element to be displayed after user reads card
const setVisibility = () => {
setPosition(position++);
};
//function to change the difficulty of a card
const difficultyHandler = (difficulty, id) => {
console.log(difficulty);
setData(
data.map((ele) => {
if (ele.ID === id) {
return { ...ele, type: difficulty };
}
return ele;
})
);
};
//useEffect for fetching data from db
useEffect(() => {
axios
.get("/api/cards")
.then((res) => {
if (res.data) {
console.log(res.data);
setData(res.data.sort(() => (Math.random() > 0.5 ? 1 : -1)));
}
})
.catch((err) => {
console.log(err);
});
}, []);
return (
<cardContext.Provider
value={{ cardData: data, setDifficulty: difficultyHandler }}
>
{data.length && (
<Card
position={position}
// dataIndex={index}
visible={setVisibility}
id={data[position].ID}
front={data[position].Front}
back={data[position].Back}
/>
)}
</cardContext.Provider>
);
}
//Card component
import { React, useState, useEffect } from "react";
import Options from "./Options";
export default function Card(props) {
//State for showing or hiding the answer
const [reverse, setReverse] = useState(false);
const [display, setDisplay] = useState(true);
//function for showing the answer
const reversalHandler = () => {
setReverse(true);
};
return (
<div>
{reverse ? (
<div className="card">
{props.front} {props.back}
<button
onClick={() => {
props.visible();
}}
>
Next Card
</button>
</div>
) : (
<div className="card">{props.front}</div>
)}
<Options
visible={props.visible}
reverse={reversalHandler}
id={props.id}
/>
</div>
);
}
//Options Component
import { React, useContext, useState } from "react";
import cardContext from "../store/cardContext";
export default function Options(props) {
const ctx = useContext(cardContext);
const [display, setDisplay] = useState(true);
return (
<>
<div className={display ? "" : "inactive"}>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("easy", props.id);
}}
>
Easy
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("medium", props.id);
}}
>
Medium
</button>
<button
onClick={() => {
setDisplay(false);
props.reverse();
ctx.setDifficulty("hard", props.id);
}}
>
Hard
</button>
</div>
</>
);
}
The setVisibility function in the Decklist component is working fine and setting the position state properly. However, I don't know how to re-render the Card component so that it acts on the position state that has changed.
One way to force a re-render of a component is to set its state to itself
onClick={() => {
props.visible();
setReverse(reverse);
}}
However this probably isn't your issue as components will automatically re-render when their state changes or a parent re-renders. This means that for some reason the Card component isn't actually changing the parent component.
So I have a Context created with reducer. In reducer I have some logic, that in theory should work. I have Show Component that is iterating the data from data.js and has a button.I also have a windows Component that is iterating the data. Anyway the problem is that when I click on button in Show Component it should remove the item/id of data.js in Windows Component and in Show Component, but when I click on it nothing happens. I would be very grateful if someone could help me. Kind regards
App.js
const App =()=>{
const[isShowlOpen, setIsShowOpen]=React.useState(false)
const Show = useRef(null)
function openShow(){
setIsShowOpen(true)
}
function closeShowl(){
setIsShowOpen(false)
}
const handleShow =(e)=>{
if(show.current&& !showl.current.contains(e.target)){
closeShow()
}
}
useEffect(()=>{
document.addEventListener('click',handleShow)
return () =>{
document.removeEventListener('click', handleShow)
}
},[])
return (
<div>
<div ref={show}>
<img className='taskbar__iconsRight' onClick={() =>
setIsShowOpen(!isShowOpen)}
src="https://winaero.com/blog/wp-content/uploads/2017/07/Control-
-icon.png"/>
{isShowOpen ? <Show closeShow={closeShow} />: null}
</div>
)
}
```Context```
import React, { useState, useContext, useReducer, useEffect } from 'react'
import {windowsIcons} from './data'
import reducer from './reducer'
const AppContext = React.createContext()
const initialState = {
icons: windowsIcons
}
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const remove = (id) => {
dispatch({ type: 'REMOVE', payload: id })
}
return (
<AppContext.Provider
value={{
...state,
remove,
}}
>
{children}
</AppContext.Provider>
)
}
export const useGlobalContext = () => {
return useContext(AppContext)
}
export { AppContext, AppProvider }
reducer.js
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
return {
...state,
icons: state.icons.filter((windowsIcons) => windowsIcons.id !== action.payload),
}
}
}
export default reducer
``data.js```
export const windowsIcons =[
{
id:15,
url:"something/",
name:"yes",
img:"/images/icons/crud.png",
},
{
id:16,
url:"something/",
name:"nine",
img:"/images/icons/stermm.png",
},
{
id:17,
url:"domething/",
name:"ten",
img:"/images/icons/ll.png",
},
{
id:18,
url:"whatever",
name:"twenty",
img:"/images/icons/icons848.png",
},
{
id:19,
url:"hello",
name:"yeaa",
img:"/images/icons/icons8-96.png",
},
]
``` Show Component```
import React from 'react'
import { useGlobalContext } from '../../context'
import WindowsIcons from '../../WindowsIcons/WindowsIcons'
const Show = () => {
const { remove, } = useGlobalContext()
return (
<div className='control'>
{windowsIcons.map((unin)=>{
const { name, img, id} = unin
return (
<li className='control' key ={id}>
<div className='img__text'>
<img className='control__Img' src={img} />
<h4 className='control__name'>{name}</h4>
</div>
<button className='unin__button' onClick={() => remove(id)} >remove</button>
</li> )
</div>
)
}
export default Show
import React from 'react'
import {windowsIcons} from "../data"
import './WindowsIcons.css'
const WindowsIcons = ({id, url, img, name}) => {
return (
<>
{windowsIcons.map((icons)=>{
const {id, name , img ,url} =icons
return(
<div className='windows__icon' >
<li className='windows__list' key={id}>
<a href={url}>
<img className='windows__image' src={img}/>
<h4 className='windows__text'>{name}</h4>
</a>
</li>
</div>
)
})}
</>
)
}
Issue
In the reducer you are setting the initial state to your data list.
This is all correct.
However, then in your Show component you are directly importing windowsIcons and looping over it to render. So you are no longer looping over the state the reducer is handling. If the state changes, you won't see it.
Solution
In your Show component instead loop over the state that you have in the reducer:
const { remove, icons } = useGlobalContext()
{icons.map((unin) => {
// Render stuff
}
Now if you click remove it will modify the internal state and the icons variable will get updated.
Codesandbox working example
I have made a basic application to practice React, but am confused as to why, when I try to delete a single component from an state array, all items after it get deleted too. Here is my basic code:
App.js:
import React from 'react'
import Parent from './Parent';
import './App.css';
function App() {
return (
<div className="App">
<Parent />
</div>
);
}
export default App;
Parent.js:
import React, { useState } from 'react';
import ListItem from './ListItem';
import './App.css';
function Parent() {
const [itemList, setItemList] = useState([])
const [numbers, setNumbers] = useState([])
const addItem = () => {
const id = Math.ceil(Math.random()*10000)
const newItem = <ListItem
id={id}
name={'Item-' + id}
deleteItem={deleteItem}
/>
const list = [...itemList, newItem]
setItemList(list)
};
const deleteItem = (id) => {
let newItemList = itemList;
newItemList = newItemList.filter(item => {
return item.id !== id
})
setItemList(newItemList);
}
const addNumber = () => {
const newNumbers = [...numbers, numbers.length + 1]
setNumbers(newNumbers)
}
const deleteNum = (e) => {
let newNumbers = numbers
newNumbers = newNumbers.filter(n => n !== +e.target.innerHTML)
setNumbers(newNumbers);
}
return (
<div className="Parent">
List of items:
<div>
{itemList}
</div>
<button onClick={addItem}>
Add item
</button>
<div>
List of numbers:
<div>
{numbers.map(num => (
<div onClick={deleteNum}>{num}</div>
))}
</div>
</div>
<button onClick={addNumber}>
Add number
</button>
</div>
);
};
export default Parent;
ListItem.js:
import React from 'react';
import './App.css';
function ListItem(props) {
const { id, name, deleteItem } = props;
const handleDeleteItem = () => {
deleteItem(id);
}
return (
<div className="ListItem" onClick={handleDeleteItem}>
<div>{name}</div>
</div>
);
};
export default ListItem;
When I add an item by clicking the button, the Parent state updates correctly.
When I click on the item (to delete it), it deletes itself but also every item in the array that appears after it <-- UNWANTED BEHAVOUR. I only want to delete the specific item.
I have tested it with numbers too (not creating a separate component). These delete correctly - only the individual number I click on is deleted.
As far as I can tell, the individual item components are saving a reference as to what the Parent state value was when they are created. This seems like very strange behaviour to me...
How do I delete only an individual item from the itemList state array when they are made up of separate components?
Thanks
EDIT: As per the instruction from Bergi, I fixed the issue by converting the 'itemList' state value to an array of objects to render (and rerender) when the list is changed instead:
const addItem = () => {
const id = Math.ceil(Math.random()*10000);
const newItem = {
id: id,
name: 'Item-' + id,
}
const newList = [...itemList, newItem]
setItemList(newList)
}
...
React.useEffect(() => {
}, [itemList]);
...
<div className="Parent">
List of items:
<div>
{itemList.map(item => {
return (<ListItem
id={item.id}
name={item.name}
deleteItem={deleteItem}
/>);
})}
...
The problem is that your deleteItem function is a closure over the old itemList, back from the moment in which the item was created. Two solutions:
use the callback form of setItemList
don't store react elements in that list, but just plain objects (which you can use as props) and pass the (most recent) deleteItem function only when rendering the ListItems
I have been looking on google a lot about how to pass props between functional components but very little information seems to be out there(either that or I don't know what keywords to input into google).
I do not need redux or data stored globally, I simply want to pass a JSON object stored in a hook from one component file to another.
I have three files one is the parent and the other two are children, I want to pass the data between the children files.
Paerent
import React, { useState } from "react";
import ShoppingPageOne from "./ShoppingPageOne";
import ShoppingPageTwo from "./ShoppingPageSecond";
function ShoppingPageContainer() {
const [pageone_Open, setPageone_Open] = useState(true);
const [pagetwo_Open, setPagetwo_Open] = useState(false);
const page_showHandler = () => {
setPageone_Open(!pageone_Open);
setPagetwo_Open(!pagetwo_Open);
};
return (
<div className="Shopping_Container">
<div className="Shopping_Box">
<h2>Online food shop</h2>
<div className="Shopping_Page_Container">
<ShoppingPageOne showOne={pageone_Open} next_ClickHandler={page_showHandler} />
<ShoppingPageTwo showTwo={pagetwo_Open} Reset_Data={page_showHandler} />
</div>
</div>
</div>
);
}
export default ShoppingPageContainer;
Child one:
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState("pageOne");
//stores quantities of items as JSON objects
const [Quantities, setQuantities] = useState({});
const [QuantiesProps, setQuantitiesProps] = useState(null)
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show("pageOne");
} else {
setPageone_show("pageOne hide");
}
}, [props.showOne]);
return (
<div className={"Shopping_Content " + pageone_show}>
{Data.map((Ingredients) => {
//updates Quanties Hook
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
return (<div className="Shopping_input" key={Ingredients.Name}>
<p>{Ingredients.Name} £{Ingredients.Price}</p>
<input onChange={handleChange.bind(this)} min="0" type="number"></input>
</div>)
})}
<div className="Shopping_Buttons">
<p onClick={props.next_ClickHandler}>Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
Child Two
import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;import React, { useState, useEffect } from "react";
import Data from '../shoppingData/Ingredients';
const ShoppingPageSecond = (props) => {
//element displays
const [pagetwo_show, setPagetwo_show] = useState("pageTwo hide");
useEffect(() => {
//resets info text
if (props.showTwo) {
setPagetwo_show("pageTwo");
} else {
setPagetwo_show("pageTwo hide");
}
}, [props.showTwo]);
return (
<div className={"Shopping_Content " + pagetwo_show}>
<div className="Shopping_Buttons">
<p onClick={props.Reset_Data}>Shop Again</p>
</div>
</div>
);
};
export default ShoppingPageSecond;
I simply want to pass the state contained in Quantities hook from Child One to Child Two when "Buy Now!" button is clicked.
What is the best approach to do doing this?
From my understand, I don't pass props between two children under the same parent. Instead, the parent holds the data, and pass the data and mutation function to children as props.
import React, { useState } from 'react';
const PageOne = ({ value, setValue }) => {
const PageOneFunction = () => {
setValue({
pageOneData: value.pageOneData + 1,
pageTwoData: value.pageTwoData + 1,
});
};
return (
<div>
<h4>Page One</h4>
<div>{value.pageOneData}</div>
<button onClick={PageOneFunction}>
Increase page one and page two value
</button>
</div>
);
};
const PageTwo = ({ value, setValue }) => {
const pageTwoFunction = () => {
setValue({
pageOneData: 0,
pageTwoData: 0,
});
};
return (
<div>
<h4>Page Two</h4>
<div>{value.pageTwoData}</div>
<button onClick={pageTwoFunction}>Reset</button>
</div>
);
};
const PageContainer = () => {
const [data, setData] = useState({
pageOneData: 0,
pageTwoData: 0,
});
return (
<div className="bg-white">
<PageOne value={data} setValue={setData} />
<PageTwo value={data} setValue={setData} />
</div>
);
};
export default PageContainer;
like this i hava a array of components need ref to trigger the comment component collapse, so i need to create some refs to reference each commentListItem, but it doesn't work, how do i do this work?
import React, { useRef, createRef } from "react";
import PropTypes from "prop-types";
import { map, isArray } from "lodash/fp";
import Divider from "#material-ui/core/Divider";
import CommentListItem from "./CommentListItem";
import CommentCollapse from "./CommentCollapse";
function CommentList({ list = [], ...props }) {
const { count = 0 } = props;
const refList = map((o) => {
/* o.ref = createRef(null); */
return o;
})(list);
const onShow = () => {
console.log(refList);
};
return (
<div className="ke-comment-list">
{map.convert({ cap: false })((o, i) => (
<div key={i} className="ke-comment-list-item">
<CommentListItem listItem={o} onShow={onShow} />
{isArray(o.child) && o.child.length ? (
<CommentCollapse {...o}>
<CommentList list={o.child} count={count + 1} />
</CommentCollapse>
) : null}
{count > 0 && list.length - 1 === i ? null : <Divider />}
</div>
))(refList)}
</div>
);
}
CommentList.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default CommentList;
there is CommentCollapse component for show or hide subcomment.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore: showMore()
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);
catch errors
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
have any idear for this situation?
is fixed, just not trigger showMore function in ref.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);