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)
Related
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>
I am getting this error while using useRef and useEffect in react js.
**how can i cleanup the useEffect in React js this is main topic of this all question **
Dropdown.js:9
Uncaught TypeError: Cannot read properties of null (reading 'contains')
at HTMLDocument.bodydroptoggler (Dropdown.js:9)
here is screenshot:
I am getting this error when i click on the button named as "drop toggler"
here is code of app.js
import React, { useState } from "react";
import Dropdown from "./components/Dropdown";
const options = [
{
label: "red color is selected",
value: "red",
},
{
label: "blue color is selected",
value: "blue",
},
{
label: "green color is seleted",
value: "green",
},
];
const App = () => {
const [dropactive, setDropactive] = useState(true);
return (
<div className="container ui">
<button
className="button ui"
onClick={() => setDropactive(!dropactive)}
>
drop toggler
</button>
{dropactive ? <Dropdown options={options} /> : null}
</div>
);
};
export default App;
and here is code of dropdown.js
import React, { useState, useRef, useEffect } from "react";
const Dropdown = ({ options }) => {
const [selected, setSelected] = useState(options[0]);
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const bodydroptoggler = (event) => {
if (ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.addEventListener("click", bodydroptoggler);
return () => {
document.removeEventListener("click", bodydroptoggler);
console.log("work");
};
}, []);
const RenderedOptions = options.map((option, index) => {
if (selected.value === option.value) {
return null;
} else {
return (
<div
className="item"
key={index}
onClick={() => {
setSelected(option);
}}
>
{option.label}
</div>
);
}
});
return (
<div ref={ref} className="ui form">
<div className="field">
<label className="text label">Select from here:</label>
<div
className={`ui selection dropdown ${
open ? "active visible" : ""
}`}
onClick={() => setOpen(!open)}
>
<i className="dropdown icon"></i>
<div className="text">{selected.label}</div>
<div className={`menu ${open ? "visible transition" : ""}`}>
{RenderedOptions}
</div>
</div>
</div>
</div>
);
};
export default Dropdown;
here is what i want to perform
i just want to hide that form by clicking on the button.
how you can run this project
just create a react app
put code of app.js to app.js of your project
dropdown.js inside the component folder
i hope this all detail will help you i you need anything more just commnet down
thanks in advance
Have you tried using optional chaining since ref.current might sometimes be undefined?
if (ref.current?.contains(event.target))
Here's a codesandbox link with the fix.
Also some additional context from React Ref docs on why sometimes the ref might be null
React will assign the current property with the DOM element when the component mounts, and assign it back to null when it unmounts.
EDIT:
This is whay useLayoutEffect is for. It runs it's contents (and cleanups) synchronously and avoids the race condition. Here's the stackblitz that proves it:
https://stackblitz.com/edit/react-5w7vog
Check out this post from Kent Dodd's as well:
One other situation you might want to use useLayoutEffect instead of useEffect is if you're updating a value (like a ref) and you want to make sure it's up-to-date before any other code runs. For example:
ORIGINAL ANSWER
It's complicated. Your code generally looks good so it took me a minute to understand why. But here's the why - the Dropdown component unmounts before the cleanup from the effect is run. So the click event still finds the handler, this time with a null reference for the ref (because the ref gets updated immediately).
Your code is correct, idomatic React - but this is an edge case that needs deeper understanding.
As the other answerer already mentioned, just add an optional check. But I thought you might like to know why.
I was playing around with React and I have a list of elements like this:
{posts.map((post) => (
<div className ="hidden" key={post.key}></div>
<button onClick = {() => showMore()}>Click to Show</button>
))}
posts is a list of json objects. The idea is that only a short description of the post would show and when the button is clicked, it shows the full body.
posts = [{
"title": "Title",
"description": "",
"thumbnail": 'images/image.png',
"body": "",
"id": 1
}]
I want to show that particular post when the button for it is clicked. This is easy to do with document.getElementById but I'm unable to do this with React.
The only way I can think of is a single useState for all the posts but this is not what I want.
This is different from just showing or hiding a single element because in this case I can't just create a single useState. I guess I could create as many useStates as there are posts but I don't really know how this would work.
Thanks!
The way I would approach this is by creating a Post component which has an internal state:
const Post = ({ post }) => {
const [showMore, setShowMore] = useState(false);
const handleShowMore = () => setShowMore((prevShowMore) => !prevShowMore);
return (
<div class="post" key={post.id}>
<p>{showMore ? post.description : post.description.substring(0, 10)}</p>
<button onClick={handleShowMore}>
Show {showMore ? "Less" : "More"}
</button>
</div>
);
};
If showMore is false (which it is by default), it will only show the first 10 characters of the string. If it is true, it will show the entire string.
Then in your parent component, I'd map over the posts and render the Post component:
const ParentComponent = () => {
return (
<div className="container">
{data.map((post) => (
<Post post={post} />
))}
</div>
);
};
Here is a example: https://codepen.io/AliKlein/pen/KKajvOW
Is this close to what you're trying to accomplish?
I am making a simple Reactjs accordion application in which collapse and expanding of each individual items are done.
Requirement:
A simple requirement is that I am in the need to toggle the text of the heading as Expand or Shrink based on the click.
If we click any of the item then the content will gets displayed in that case the text changes to Shrink as because the accordion gets opened so the Shrink title is given for closing of the accordion.
Complete working example:
https://codesandbox.io/s/react-accordion-forked-lcyr0
In the above example I have used the following code to change the text,
Accordion.js
import React, { useState } from "react";
import Text from "./text";
import Heading from "./heading";
import getAccordion from "./GetAccordion";
const Accordion = getAccordion(1);
const accordionData = [
{
id: 1,
content: "This is a first content"
},
{
id: 2,
content: "This is a second content"
},
{
id: 3,
content: "This is a third content"
}
];
const NormalAccordion = () => {
const [toggleValue, setToggleValue] = useState(-1);
const toggleHandler = (index) => {
setToggleValue(index);
};
return (
<div>
{accordionData.map((item, index) => (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={() => toggleHandler(index)}
>
{toggleValue !== index ? `Expand` : `Shrink`}
</div>
</Heading>
<Text>{item.content}</Text>
</Accordion>
))}
</div>
);
};
export default NormalAccordion;
And this line {toggleValue !== index ? `Expand` : `Shrink`} changes the text once but after that it doesn't make any changes on further toggle over the title(Expand/Shrink).
Kindly help me to achieve the result of toggling the text between Expand and Shrink based on the respective clicks.
You should reset the toggleValue if the item is already clicked :
const toggleHandler = (index) => {
index===toggleValue?setToggleValue(-1): setToggleValue(index);
};
then use condition to render the current content:
<Text>{toggleValue === index && item.content}</Text>
and simplify the Text component to this one :
<div style={{ ...this.props.style }}>
<div className={`content ${this.props.text ? "open" : ""}`}>
{this.props.children}
</div>
</div>
You should use boolean value as an accordion can have 2 values.
Idea:
Create a standalone accourdion component which defines a behavior of expand/ collapse.
Create a wrapper view component that calls this this component.
Pass your data as a prop from this view instead of using a local hard coded object.
Benefit of this approach is that you have 2 modular components that work only on props. These can be exported later to their own standalone library.
Ideally, a component should be dumb and should only know its own behavior based on state and props.
Updated sandbox
Accoirdion component changes
const [toggleValue, setToggleValue] = useState(false);
const toggleHandler = () => {
setToggleValue(!toggleValue);
};
Accordion view changes
const AccordionView = ({ accordionData }) => {
return (
<div>
{accordionData.map((item) => (
<NormalAccordion content={item.content} />
))}
</div>
);
};
Constants: I create a folder called constants that will hold hardcoded objects and then we pass it using props from index.js
export const accordionData = [{
id: 1,
content: "This is a first content"
},
{
id: 2,
content: "This is a second content"
},
{
id: 3,
content: "This is a third content"
}
];
Changes in index.js
import { accordionData } from '../constants/accordion-data'
...
<NormalAccordion accordionData={ accordionData } />
I have a list of cities and I'm trying to include a modal with a trash can icon to delete the city next to each item. The problem I have is that the modal seems to pick the last item of the list for EVERY item on the list.
When you click on the icon on any element on the list the confirmation modal always points to the last element on the list and I'm not sure what am I doing wrong. :(
I tried using a Confirm element instead only to find out it's using the modal underneath and I get the same results.
Any gurus around who can help me troubleshoot this will be greatly appreciated!
import React, { useState, useCallback } from "react";
import { List, Icon, Modal, Button } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
const CitiesList = () => {
const [deleteButtonOpen, setDeleteButtonOpen] = useState(false);
const cities = [{ name: "London" }, { name: "Paris" }, { name: "Porto" }];
const handleConfirmDeleteCityModal = useCallback(city => {
console.log("[handleConfirmDeleteCityModal] city", city);
// dispatch(deleteCity(city))
setDeleteButtonOpen(false);
}, []);
const showDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(true);
}, []);
const handleCancelDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(false);
}, []);
return (
<List>
{cities.map(c => (
<List.Item>
<List.Content className="list-item-content">
<List.Header as="h4">{c.name}</List.Header>
</List.Content>
<List.Content floated="left">
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
<Modal.Header>{`Delete City ${c.name}`}</Modal.Header>
<Modal.Content>
<p>Are you sure you want to delete this city?</p>
</Modal.Content>
<Modal.Actions>
<Button negative>No</Button>
<Button
positive
icon="checkmark"
labelPosition="right"
content="Yes"
onClick={() => handleConfirmDeleteCityModal(c)}
/>
</Modal.Actions>
</Modal>
</List.Content>
</List.Item>
))}
</List>
);
};
export default CitiesList;
Here is the example: https://codesandbox.io/s/optimistic-borg-56bwg?from-embed
The problem is this:
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
you use single flag deleteButtonOpen for controlling visibility of all modals. When you set it to true I suppose all modals are opened and you see only the latest one.
Normally I would render single modal and pass as props content of which item I want to show inside.
But if not using separate open flag for each modal should fix it, e.g. https://codesandbox.io/s/vigilant-banzai-byc4t