Passing an Array from one UseState to Another - javascript

I'm currently trying to figure out how to pass an array from one useState object to another across two different components. In my first useState I have an array called imagesOriginal, which gets filled with file paths dynamically to various different images like in the following:
[
"https://source.unsplash.com/WLUHO9A_xik/900x900",
"https://source.unsplash.com/R4K8S77qtwI/900x900",
"https://source.unsplash.com/jJGc21mEh8Q/900x900"
]
In my App.js, I construct it like so.
import React, { useCallback, useState } from 'react';
import ShowImage from './ShowImage.jsx';
import DropBox from './DropBox.js';
function App() {
const [imagesOriginal, setImages] = useState([]);
const onDrop = useCallback((acceptedFiles) => {
acceptedFiles.map((file, index) => {
const reader = new FileReader();
reader.onload = function (e) {
setImages((prevState) => [
...prevState,
{ id: index, src: e.target.result },
]);
};
reader.readAsDataURL(file);
return file;
});
}, []);
return (
<div className="App">
<div class="parent">
<div>
<h3>Originals</h3>
<DropBox onDrop={onDrop} />
<ShowImage images={imagesOriginal}/>
</div>
</div>
</div>
);
}
export default App;
The main issue comese in the ShowImage.jsx, where I want to pass that array of images to another useState, as I need to use both the array and the setItems to sort the array with a new order.
import React, { useState } from 'react';
import {
DndContext,
closestCorners,
MouseSensor,
TouchSensor,
DragOverlay,
useSensor,
useSensors,
} from '#dnd-kit/core';
import "./ShowImage.css"
import {arrayMove, SortableContext} from '#dnd-kit/sortable';
import {SortablePhoto} from './SortablePhoto.jsx';
const ShowImage = ({images}) => {
const [items, setItems] = useState([]);
setItems([...images]);
const [activeId, setActiveId] = useState(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
return(
<div class="scroll">
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={items} strategy={() => {}}>
<div columns={1}
style={{
display: "grid",
gridAutoRows: `100px`,
gridGap: 10
}}
>
{items.map((url, index) => (
<SortablePhoto key={url.src} url={url.src} index={index}/>
))}
</div>
</SortableContext>
</DndContext>
</div>
);
function handleDragStart(event) {
setActiveId(event.active.id);
}
function handleDragOver(event) {
const {active, over} = event;
if (active.id !== over.id) {
setItems((items) => {
const oldIndex = items.indexOf(active.id);
const newIndex = items.indexOf(over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}
function handleDragEnd(event) {
setActiveId(null);
}
function handleDragCancel() {
setActiveId(null);
}
};
export default ShowImage;
I've tried using the line setItems([...images]); to try and pass the new items in, and also const [items, setItems] = useState(images);, but It never seems to update the items array. I'm probably doing something really stupid, but any help would be greatly appreciated.

You can create a function in your App component that wraps your setItems state modifier and pass this function as a prop to your nested ShowImage component where you could use it to manipulate the state. This way you won't need to maintain 2 different states.
// App.js
function App() {
const [imagesOriginal, setImages] = useState([]);
const setImagesWrapper = useCallback(val => {
setImages(val);
}, [setImages]);
//...
return (
<div className="App">
<div class="parent">
<div>
<h3>Originals</h3>
<DropBox onDrop={onDrop} />
<ShowImage
images={imagesOriginal}
setImages={setImagesWrapper}
/>
</div>
</div>
</div>
);
}
export default App;
// ShowImage.js
const ShowImage = ({ images, setImages }) => {
const [activeId, setActiveId] = useState(null);
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
// ...
};
export default ShowImage;

Related

Why is my global State not updating across other components

So I am trying to store a global state using context to allow me to use the same state across different components.
The issue I am having is that when I set the global state in 1 component and try to access it in the other component to use the state. It appears to be null and I cannot figure out why?
The first component where I set the global state in will always be rendered before the component shown that seems to have an empty value for the global state.
GlobalStateProvider component:
import React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
const defaultActivitiesState = [];
const globalStateContext = React.createContext(defaultActivitiesState);
const dispatchStateContext = React.createContext([]);
export const useGlobalState = () =>
[
React.useContext(globalStateContext),
React.useContext(dispatchStateContext)
];
const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = React.useReducer((state, newValue) => (state, newValue),
defaultActivitiesState
);
return (
<globalStateContext.Provider value={state}>
<dispatchStateContext.Provider value={dispatch}>
{children}
</dispatchStateContext.Provider>
</globalStateContext.Provider>
);
}
export default GlobalStateProvider;
Component I set the global state in:
import react from "react";
import { useState, useEffect, useMemo } from "react";
import { MapContainer, TileLayer, Popup, Polyline } from "react-leaflet";
import axios from "axios";
import polyline from "#mapbox/polyline";
import MapComp from "./MapComp";
import { useGlobalState } from "./GlobalStateProvider";
function Map() {
// ------- global state
const [activities, setActivities] = useGlobalState(); // global state
//const [activities, setActivities] = useState([]);
//const [polylines, setPolylines] = useState(null); // as empty array value is still truthy
const [isLoading, setIsLoading] = useState(true);
const [mapMode, setMapMode] = useState("light");
const [mapStyle, setMapStyle] = useState(
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
);
const [showMarkers, setShowMarkers] = useState(true);
useEffect(() => {
setActivitieData();
console.log("activities after useEffect", activities)
}, []);
const polylines = useMemo(() => {
console.log("activities inside memo", activities)
console.log("activities.len =", activities.length);
if (activities.length) {
console.log("past len");
const polylineArray = [];
for (const item of activities) {
const polylineData = item.map.summary_polyline;
const activityName = item.name;
const activityType = item.type;
polylineArray.push({
positions: polyline.decode(polylineData),
name: activityName,
activityType: activityType,
});
}
setIsLoading(false);
return polylineArray;
}
return null;
}, [activities]);
const toggleMarkers = () => {
setShowMarkers((show) => !show);
};
const getActivityData = async () => {
console.log("calling")
const response = await axios.get(
"http://localhost:8800/api/"
);
return response.data;
};
const setActivitieData = async () => {
const activityData = await getActivityData();
setActivities(activityData);
console.log("Global activities state = ", activities);
};
return !isLoading && polylines ? (
<>
<div className="select-container">
<button className="toggle-markers" onClick={() => toggleMarkers()}>
Toggle Markers
</button>
</div>
<MapComp
className={`${mapMode}`}
activityData={{ polylines }}
showMarkers={showMarkers}
/>
</>
) : (
<div>
<p>Loading...</p>
</div>
);
}
export default Map;
component that has an empty value for global state:
import React from 'react';
import { useGlobalState } from './GlobalStateProvider';
function ActivityList() {
const [activities, setActivities] = useGlobalState();
let displayValues;
displayValues =
activities.map((activity) => {
return (
<div>
<p>{activity.name}</p>
<p>{activity.distance}m</p>
</div>
);
})
return (
<>
<p>Values</p>
{displayValues}
</>
);
}
export default ActivityList;
App.js:
function App() {
return (
<GlobalStateProvider>
<div className="App">
<NavBar />
<AllRoutes />
</div>
</GlobalStateProvider>
);
}
export default App;

Error "elements.map is not a function" when trying to update array

I am receiving an error of "HomePage.jsx:16 Uncaught TypeError: elements.map is not a function" when trying to change the boxtitle for the array component. I have tried moving around functions and such but nothing seems to work.
I basically only want to change the title for the certain array object with the same boxid. Below is my code.
HomePage.jsx:
import react from 'react';
import { useEffect, useContext } from 'react';
import '../App.css';
import Todobox from './Todobox';
import { ElementContext } from '../ElementContext';
export default function HomePage(){
const { elements, setElements, newElement, elementId } = useContext(ElementContext);
return(
<div className='page-container'>
<div className='header'>
<a className='header-title'>Trello Clone!</a>
<a className='header-button' onClick={newElement}>Create a list</a>
</div>
<div className='element-field'>
{elements.map((element) => <Todobox key={element.boxid} boxid={element.boxid} boxtitle={element.boxtitle}/>)}
</div>
</div>
)
}
Todobox.jsx:
import React from 'react';
import Item from './Item';
import { useState, useContext } from 'react';
import '../App.css';
import { ElementContext } from '../ElementContext';
export default function Todobox({ boxtitle, boxid }){
const { elements, setElements } = useContext(ElementContext);
const [boxheader, setBoxHeader] = useState();
const handleSubmit = (e) => {
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value)
return obj
}})
setElements({...object, boxtitle: boxheader})
}
const handleKeydown = (e) => {
if(e.keyCode == 13 && e.shiftKey == false){
e.preventDefault();
handleSubmit(e)
}
}
return(
<div className='element-box'>
<textarea className='element-title-input' placeholder={boxtitle} onKeyDown={handleKeydown}/>
<Item />
<textarea
className='element-input'
type='text'
placeholder={`Add item... ${boxid}`}
onClick={() => {console.log(boxid)}}
/>
</div>
)
}
ElementContext.js:
import React, { createContext, useState } from 'react';
import Todobox from './components/Todobox';
export const ElementContext = createContext();
export const ElementContextProvider = ({children}) => {
const [elements, setElements] = useState([]);
const [elementId, setElementId] = useState(1);
const [title, setTitle] = useState('Add title...');
const [refDict, setRefDict] = useState({});
const newElementId = (elements) =>{
setElementId(elementId + 1);
console.log(elementId)
}
const newElement = () => {
newElementId();
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
const value = {
elements,
setElements,
newElement,
elementId,
};
return(
<ElementContext.Provider value={value}>
{children}
</ElementContext.Provider>
)
};
Code Sandbox
Any help is appreciated since I am new and still learning! :)
Few points to handle
const newElement = () => {
newElementId(); // this wont update as react batches the state updates
// try using userRef for elementId
// create new element id here, and then set it
// const newId = elementId + 1
// setElementId(newElementId)
if (!refDict[elementId]) {
setElements(prev => [...prev, { boxtitle: title, boxid: elementId }]);
setRefDict((prev) => ({...prev, [elementId]: true}));
}
console.log(elements);
};
on submit
const object = elements.find(obj => {
if (obj.boxid === boxid){
setBoxHeader(e.target.value) // wont update the as React will likely batch the state updates
return obj
}})
setElements({...object, boxtitle: boxheader}) // is this an array ?
instead try
const object = elements?.find(obj => obj.boxid === boxid)
if (object) {
setBoxHeader(e.target.value)
setElements([ object, { boxtitle: e.target.value, boxId: elementId } ]) // what is the structure of array elements
}
you will find the new React documentation about updating arrays useful

Reducer/Context Api

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

How do I delete a component from a React state array without deleting rest of array?

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

Passing props/state between two functional child components in react?

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;

Categories