Scrolling to latest rendered component - reactjs

how can I pass ref to latest rendered component in this situation so page will scroll to him after its render? The example below obviosuly doesn't work but I dont have any idea how to do it.
Edit: Added ShortenedLink component according to comments.
const ref = useRef(null);
useEffect(() => {
ref.current.scrollIntoView();
}, [linkArr]);
return(
<LinkShortenerContainer >
<LinkShortener
setLinkArr={setLinkArr}
/>
<AnimatePresence>
{linkArr.map((item) => {
return (
<ShortenedLink
ref={ref}
setLinkArr={setLinkArr}
key={item.id}
id={item.id}
long={item.long}
short={item.short}
/>
);
})}
</AnimatePresence>
</LinkShortenerContainer>
)
export default function ShortenedLink({ long, short, id, setLinkArr }) {
const [isCopied, setIsCopied] = useState(false);
const handleClick = () => {
navigator.clipboard.writeText(short);
setIsCopied(true);
};
const removeLink = () => {
setLinkArr((prev) =>
prev.filter((item) => {
return item.id !== id;
})
);
};
return (
<ShortenedLinkContainer
as={motion.div}
initial={{ x: -1500, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 1500, opacity: 0 }}
>
<LinkToShorten>{long}</LinkToShorten>
<ReadyLink>{short}</ReadyLink>
<Button
size={"medium"}
text={isCopied ? "Copied !" : "Copy !"}
onClick={() => handleClick()}
></Button>
<IconContainer>
<FontAwesomeIcon onClick={() => removeLink()} icon={faX} size={"sm"} />
</IconContainer>
</ShortenedLinkContainer>
);
}

I had a same requirement when writing my chat app: rolling to the latest message.
My way is to put an empty div after all messages. then all I need is to scroll to that div instead of putting ref in every newly created message.

Related

React gallery App. I want Add tags to an image individually but the tag is being added to all images. How can I solve this?

**> This is my Gallery Component **
import React, {useState} from 'react';
import useFirestore from '../hooks/useFirestore';
import { motion } from 'framer-motion';
const Gallery = ({ setSelectedImg }) => {
const { docs } = useFirestore('images');
here im setting the state as a Tags array
const [tags, setTags] = useState([""]);
const addTag = (e) => {
if (e.key === "Enter") {
if (e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = "";
}
}
};
functions for adding and removing Tags
const removeTag = (removedTag) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
return (
<>
<div className="img-grid">
{docs && docs.map(doc => (
< motion.div className="img-wrap" key={doc.id}
layout
whileHover={{ opacity: 1 }}s
onClick={() => setSelectedImg(doc.url)}
>
here Im adding the Tag input to each Image...the problem is that when adding a Tag is added to all the pictures. I want to add the tags for the image that I´m selecting.
<div className="tag-container">
{tags.map((tag, ) => {
return (
<div key={doc.id} className="tag">
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
<motion.img src={doc.url} alt="uploaded pic"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
</motion.img>
</motion.div>
))}
</div>
</>
)
}
export default Gallery;
The tags array that you are using to store values entered by the user are not unique with respect to each image item. Meaning, every image item in your program is using the same instance of the tags array, what you need to do is
Either create an object that stores an array of tags for each image:
const [tagsObj, setTagsObj] = {}, then while adding a new tag for say image_1, you can simply do setTagsObj(prevObj => {...prevObj, image_1: [...prevObj?.image_1, newTagValue]},
Or create an Image Component which would then handle tags for a single image:
Gallery Component:
{
imageList.map(imageEl =>
<ImageItem key={imageEl} image={imageEl} />
)
}
ImageItem Component:
import {useState} from 'react';
export default function ImageItem({image}) {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === "Enter") {
const newVal = e.target.value;
if (newVal.length > 0) {
setTags(prevTags => [...prevTags, newVal]);
e.target.value = '';
}
}
};
const removeTag = (removedTag) => {
setTags(prevTags => prevTags.filter((tag) => tag !== removedTag));
}
return (
<div style={{margin: '12px', padding: '12px', width: '100px', height:'100px', display:'flex', flexDirection: 'column', alignItems:'center'}}>
<span>{image}</span>
{tags.map((tag, index) => {
return (
<div key={tag+index}>
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
);
}
Refer this sandbox for ease, if available Gallery unique image tags sandbox
I suggest using the second method, as it is easy to understand and debug later on.
I hope this helps, please accept the answer if it does!

Why is my JSX.Element not updating when the state is updated unless I wrap it in another JSX.Element?

I am working on a typescript react project, both of which are new to me. I have the following model viewer and the type below it that consumes it. I deleted some of the bottom type so it is easier to read, but basically, getData gets called in useEffect. When the data is obtained, getData calls selectModule which, among other things, gets images from the data and sets shownImages. This all works as expected and shownImages is being set with the data expected. But when viewer 3d gets created, shownImages is not yet set. And it does not get recreated when shown images is set. However, if I replace <Viewer3D items={shownImages} /> with <Review3dComponent></Review3dComponent>, it does get recreated when shownImages is set. And I would like to know why Review3dComponent gets updated but Viewer3D does not.
interface Viewer3DProps {
items: Image[] | Image;
}
const Viewer3D = ({ items }: Viewer3DProps) => {
return (
<div className="threed-viewer">
<OBJModel
items={items}
enableTransform={true}
position={{ x: 0, y: 0, z: 0 }}
texPath=""
style={{ height: `100% !important`, width: `100% !important` }}
/>
</div>
);
};
export default Viewer3D;
export const File3dReview: NextPage = (): ReactElement => {
{
const [shownImages, setShownImages] = useState<Image[]>([]);
const selectModule = (module: IModule) => {
setCurrentModule(module);
setCurrentGroups(module.group);
const images: Image[] = module.group.flatMap(
(group) => group.images,
) as unknown as Image[];
setShownImages(images);
console.log(`set images`, images);
};
const getData = useCallback(async () => {
//gets data and calls select module
}, []);
useEffect(() => {
getData();
}, [getData]);
const Review3dComponent = () => {
return (
<div>
<Viewer3D items={shownImages} />
</div>
);
};
return (
<div>
<div>
<header className="app-header">
<div className="app-navigation">
<Space size="middle">
<div className="ic-24 color-secondary">
<FiArrowLeft
onClick={() => {
router.back();
}}
/>
</div>
<div>{name}</div>
</Space>
<nav>
<Button shape="circle" onClick={toggleDrawer}>
<MenuOutlined />
</Button>
</nav>
</div>
</header>
</div>
<Viewer3D items={shownImages} />
{/* <Review3dComponent></Review3dComponent> */}
<DrawerComponent></DrawerComponent>
</div>
);
}
};
export default File3dReview;

How to use two functions in onClick in Framer Motion ReactJS

I can't understand why I can't pass those two functions in onClick event in my ParsedColors Component. Only one is running. And if I change the places of the functions, the other one is running only.
Here are the functions:
const ParsedColors = props => {
const [selected, setSelected] = useState('');
const handleClick = ([color, productId]) => {
setColor([productId, color])
setSelected(color)
}
return(
<AnimateSharedLayout>
<ul>
{props.product.color.map(color => {
const parsed = JSON.parse(color)
return(
<Color
key={parsed.value}
color={parsed.value}
isSelected={selected === parsed.value}
onClick={() => handleClick([parsed.value, props.product._id])}
/>
)
})}
</ul>
</AnimateSharedLayout>
)
}
const Color = ({ color, isSelected, onClick }) => {
return (
<li className="item" onClick={onClick} style={{ backgroundColor: color}}>
{isSelected && (
<motion.div
layoutId="outline"
className="outline"
initial={false}
animate={{ borderColor: color }}
transition={spring}
/>
)}
</li>
);
}
const spring = {
type: "spring",
stiffness: 500,
damping: 30
};
Here is where I use the component:
<div className="product--Card--Layout">
<div className="product--Colors--Container">
<ParsedColors product={product}/>
</div>
...

React Material-UI menu anchor lost because of re-rendered by react-window

I'm building an infinite loading list of users with react-window. In the list, every item has an icon button from Material-UI for further action.
But I can't mount the menu near the icon as the icon button would be re-rendered when setting anchorEl for the menu to be opened. A gif clip:
The question is related to React Material-UI menu anchor broken by react-window list but has more HOC. The code is listed here. I wish I could use my codesandbox for demonstration but the react-measure keeps growing height.
function App() {
const [anchorEl, setAnchorEl] = useState(null);
const openMenu = React.useCallback(e => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
console.log("target", e.currentTarget);
}, []);
const handleClose = () => {
setAnchorEl(null);
};
const [items, setItems] = React.useState([]);
const isItemLoaded = index => {
const c = index < items.length;
// console.log("isItemLoaded", index, c);
return c;
};
const loadMoreItems = (startIndex, stopIndex) => {
console.log("loadMoreItems", startIndex, items);
setItems(items.concat(Array(10).fill({ name: "1", size: startIndex })));
};
const innerET = React.forwardRef((props, ref) => (
<div ref={ref} {...props} />
));
const Row = React.useCallback(
({ index, style }) => {
console.log("Row", items, index);
return items[index] ? (
<ListItem style={style} key={index}>
<Button variant="contained" color="primary" onClick={openMenu}>
Row {index}: {items[index].size}
</Button>
</ListItem>
) : null;
},
[items, openMenu]
);
const innerListType = React.forwardRef((props, ref) => (
<List ref={ref} {...props} />
));
return (
<div className="App">
<div className="ceiling">Something at top</div>
<div className="interest">
<Menu anchorEl={anchorEl} onClose={handleClose} />
<Measure bounds offset>
{({ measureRef, contentRect }) => {
const height = Math.min(
contentRect && contentRect.offset
? document.getElementById("root").getBoundingClientRect()
.height - contentRect.offset.top
: itemSize * items.length,
itemSize * items.length
);
console.log(
"bounds",
height,
contentRect.bounds,
contentRect.offset
);
return (
<div>
<div />
<div ref={measureRef} className="measurement">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={height}
width={
contentRect.bounds !== undefined &&
contentRect.bounds.width !== undefined
? contentRect.bounds.width
: -1
}
itemCount={itemCount}
itemSize={itemSize}
onItemsRendered={onItemsRendered}
ref={ref}
innerElementType={innerET}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
}}
</Measure>
</div>
</div>
);
}
As far as I understand, the ripple effect would trigger a re-render in the box with the first click. Moreover, the second click after the re-render upon clicking would not trigger a re-render. That feels even more peculiar to me.
EDIT: I fixed the first sandbox. And by using Material UI's list, this issue is reproducible. https://codesandbox.io/s/blissful-butterfly-qn3g7
So the problem lies in using innerElementType property.
It turns out that a hook is needed.
const innerListType = React.useMemo(() => {
return React.forwardRef((props, ref) => (
<List component="div" ref={ref} {...props} />
));
}, []);
To fix my problems, hooks for handling events are needed to be handled more carefully.

useRef.current.contains is not a function

I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.

Resources