How can I resolve a single reference in react? - javascript

fruit json value.
console.log(fruit)
[
{
content: "apple",
information: [
{
calorie: "122"
},
],
},
{
content: "banana",
information: [
{
calorie: "221"
},
],
},
{
content: "mango",
information: [
{
calorie: "52"
},
],
},
];
Create a button by running the map function on the information on the fruit.
The dynamically created button is {value.calorie} first button 122 second button 221 third button 52 is created.
And the content also maps function to fruit and puts {fruit.content} as the argument value in the button.
And set useRef so that when the value.calorie button is clicked, the fruit content button is also clicked.
const Content = useRef(null);
const Calorie = useRef(null);
const onClickContent = (content) => {
console.log(content);
};
const onClickCalorie = (calorie) => {
Content.current.click();
console.log(calorie);
};
return (
<>
fruit.map((Fields, index) => (
<>
<p>{Fields.content}</p>
<button style={{"display": "none"}} ref={Content} onClick={() => onClickContent(Fields.content)}></button>
</>
{Fields.information.map((Value, key) => {
return (
<>
<p>{Value.calorie}</p>
<button onClick={() => onClickCalorie(Value.calorie)}>{Value.calorie}</button>
</>
)
})}
))
</>
)
The problem is that when you click the 122 displayed on the button, the content is also output to the console, apple should be output, but mango is output.
If you click the button with calorie 221, banan should be output, but mango is output to the console. This is said to be because of a single reference.
How can I solve the single reference function? I do not know.

To me it looks like you can remove the ref, as all that it's currently being used for is to trigger a click event on your first (content) button when you click on your second (calorie) buttons. If that's the case, you can instead call onClickContent() manually inside of onClickCalorie(). First, you can update your onClickCalorie() to pass through the data that onClickContent() needs:
<button onClick={() => onClickCalorie(Value.calorie, Fields.content)}>{Value.calorie}</button>
Then, in your onClickCalorie(), update the parameters, and call onClickContent() manually to fire the same function you would trigger if you clicked your Content button:
const onClickCalorie = (calorie, content) => {
onClickContent(content);
// ... do stuff specific to clicking on the calorie button ...
console.log(calorie);
};
This way, you don't need to use any refs, as you don't need to explicitly refer to any DOM elements.
If the only thing that your onClickCalorie() function does is call/trigger onClickContent(), then you can remove the onClickCalorie() function and call onClickContent() directly:
<button onClick={() => onClickContent(Fields.content)}>{Value.calorie}</button>
Otherwise, if you really need to use refs, you can follow this approach where you can create a ref for each of your buttons:
const contentBtns = useRef([]);
and then within a useEffect() update the array size if the fruit array changes:
useEffect(() => {
contentBtns.current = contentBtns.current.slice(0, fruit.length);
}, [fruit]);
and then populate the array stored in contentBtns.current so that each index stores the button element:
<button
style={{"display": "none"}}
ref={el => contentBtns.current[index] = el}
onClick={() => onClickContent(Fields.content)}>
</button>
Now when you call onClickCalorie, you can refer to a specific button to click based on the index. You will need to pass through the index into your onClickCalorie() to have access to it:
const onClickCalorie = (calorie, index) => {
contentBtns.current[index].click();
console.log(calorie);
};

Related

Rendering a new component by adding data to Formik values object

I have a speed dial's icon that, when clicked, should add an accordion item to the accordion list.
Its onClick attribute calls a handleClick function that currently adds an object to formik.values.
The object is added to the values map as expected but the user needs to refresh the screen in order to see the new accordion item.
I guess I'm not properly handling the change of state in the form.
I know formik exposes the handleChange method but I'm not sure if and how it should be used in this scenario.
function AddSpeedDial({formik}) {
const dialActions = [
{
icon: <DinnerDiningIcon />,
name: 'New Dish',
handleClick: () => {
const dishCardsLength = Object.keys(formik.values.dishCards).length;
formik.values.dishCards[dishCardsLength+1] = {
dishName: ''
}
}
}
];
return (
<Box>
<SpeedDial>
{dialActions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
onClick={action.handleClick}
/>
))}
</SpeedDial>
</Box>
);
}

how to only display the information with matched id inside map method

I found myself stupid and could not get my head around with the logic.
I would like to show a few info triggered byonClick, and only those with matched id.
For example, if I click on the button with id of 1, it would only want to show values in that specific object with id:1 like description, library, etc. Right now, all the data are displayed, and because I am using component in material ui, every drawer component are displayed on top of each other (overlapping).
I know the reason causing this is because I have the drawer component inside the map method, but what could be potential solution?
Below are my simple code,
The structure of my data looks like this,
export const projectdata = [
{
id: 1,
title: "",
subtitle: "",
thumbnail: "",
description:
"",
tech: [
],
library: [""],
WebsiteUrl: "",
GitHubUrl: "",
},
...more data with sequential id number...
]
Original:
const handleDrawerOpen = (event) => () => {
setOpen(!open);
};
...
...
<SwipeableDrawer
open={open}
onClose={handleDrawerOpen(false)}
onOpen={handleDrawerOpen(true)}>
***...only show data with matched id...***
</SwipeableDrawer>
I have already map the array to display the data on webpage inside a div like this,
<div>
{ projectdata?.map(({ id, title, subtitle, thumbnail, description, tech, WebsiteUrl, GitHubUrl, library, index }) => (
<>
<Card>
...some info displayed here...
<button onClick={handleDrawerOpen(id)}></button>
</Card>
<SwipeableDrawer>
***...only show data with matched id...***
</SwipeableDrawer>
</>
))}
<div>
One solution I can think of:
with useState(), pass in id as a prop
const [projectDetailId, setProjectDetailId] = useState(null);
const [projectDetailPage, setProjectDetailPage] = useState(false);
const handleDrawerOpen = (id) => {
setProjectDetailId(id);
setProjectDetailPage(true);
};
const handleDrawerClose = () => {
setProjectDetailId(null);
setProjectDetailPage(false);
};
...
...
{projectDetailId === id ?
<SwipeableDrawer
open={projectDetailPage}
onClose={handleDrawerClose}
></SwipeableDrawer>
: null
}
However, this will trigger strange behavior of the drawer (lagging and no animation), especially with mobile device.
Possibly due to this logic projectDetailId === id ? true : false.
Ok, after your update, your problem is that you create multiple drawers, one for each id. When you click on open and set the open prop to true, all your drawers use this same prop so they all open.
You should move the Drawer out of the for and only create one, and send your object that has the id as a prop to the content of the drawer you have.
something like:
const handleDrawerOpen = (yourData) => () => {
setOpen(!open);
setYourData(yourData)
};
...
// and somewhere on your code
<SwipeableDrawer open={open}>
<SomeComponentToShowTheData data={yourData}/>
</SwipeableDrawer>

React DID NOT update components after update list state (by hook)

When use useState hook to store list of object (ex. [{ a:1 }, { a:2 }]), If I change list's element(object)'s content, react DO NOT update component.
For example in below code,
If I press first button, first h1 component's content will be 24. BUT even though I press first button, first h1 component component DO NOT update.
If I press second button after press first button, component DO update.
const [tempList, setTempList] = useState([
{ a: 1 },
{ a: 2 }
])
return (
<div>
{
tempList.map((item, idx) => {
return <h1>{item.a}</h1>
})
}
<button onClick={() => {
let temp = tempList;
temp[0]['a'] = 24
setTempList(temp)
}}>modify list</button>
<button onClick={() => {setTempList(...tempList, {a: 3})}}>insert list</button>
</div>
)
I already used useReducer hook. But it is not solution.
How Can I update component?
React re-render the component when the state or props change. And it determines that the state has changed by only looking at the memory address of the state.
In the callback of the first button, by declaring the variable temp, you are only creating a shallow copy of the tempList array. Therefore, even after modifying the first object, the id of the array does not change and react does not know that the state have been changed.
And also, by putting a callback in the setState function, you can always have a fresh reference to the current state:
const [state, setState] = useState(0);
setState(state+1) <-- the state can be stale
setState(state=>state+1) <-- increment is guaranteed
Try building an another array:
<button onClick={()=>{
setTempList(tempList=>tempList.map((item,ind)=>{
if(ind===0){
return {a:24};
}
else{
return item;
}
})
}}>modify list</button>
You had a syntax error in the second callback. In addition to the fix, I recommend again to put a callback function to the setTempList function.
<button onClick={() => {
setTempList(tempList=>[...tempList, {a: 3}])
}}>insert list</button>
You seems to be updating same reference of object instead of pushing a new object. Try this -
const [tempList, setTempList] = useState([
{ a: 1 },
{ a: 2 }
])
return (
<div>
{
tempList.map((item, idx) => {
return <h1>{item.a}</h1>
})
}
<button onClick={() => {
let temp = [...tempList];
temp[0] = { a: 24 };
setTempList(temp)
}}>modify list</button>
<button onClick={() => {setTempList(...tempList, {a: 3})}}>insert list</button>
</div>
)

Toggle view dynamically on click ReactJs

I have mapped list of data from JSON. When I clicked on of the item it should open a crawl with additional details from the same JSON file. I am able to map everything one I clicked bit I was not able to toggle. How do I do toggling.
This is my render method
render() {
return (
<div>
<h1>API</h1>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}
onClick={this.handleCrawl}>
{api.title}
</div>
))}
</div>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}>
{this.state.showCrawl[api.id] && (
<SwaggerUI url={api.opening_crawl}/>
)}
</div>
))}
</div>
</div>
);
}
This is the method for toggling. When I clicked an item the SwaggerUI component shows up and If I clicked the same link it hides.
The problem is if I clicked the 2nd link 1st link still shows. I need other view to be closed.
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
just don't spread the previous state's props.
try this:
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { [id]: !current.showCrawl[id] }
}));
};
Because in your code:
initial state:
{showCrawl: {}}
Say first time you click the first one(id: 1), your state become:
{showCrawl: {1: true}}
then u click the second one(id: 2)
{showCrawl: {1: true, 2: true}}
That's not your expected. Right?
So just don't spread the property, it should be going well.
In general, you can show or hide an element in a react component like this:
{this.state.showComponent ? (<Component/>) : (null)}
as an alternative, you can control the hiding/showing of the element in the component itself, with a show prop:
<Component show={this.state.showComponent} />
-- edit
I think I misunderstood your problem. Your problem is that you only want SwaggerUI to show for one thing at a time, but it's showing for multiple.
This is because of the way you designed your function,
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
You're only ever ADDING ids to showCrawl, not changing the ids that you toggled previously. You'll have to fix that function

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories