I have a React application that includes a Bootstrap Offcanvas component with embedded Tabs. For the Tabs and Content, they are created from an array of Table components which is mapped to create them dynamically. These Table components take a container as input, which I used a Ref to bind them to the dynamically generated Tabs. The first render works perfectly, however; when I close the Offcanvas and open it again, the Tab content is now empty. I'm assuming this is an issue with the Ref.
Here is the code for the Tab component.
import React, { useEffect, useRef } from 'react';
import Tabs from "react-bootstrap/Tabs";
import Tab from "react-bootstrap/Tab";
function LayerResults({featureTables, mapView, placeFeatureTables}) {
const resultRef = useRef(featureTables.map(() => React.createRef()));
useEffect(()=> {
if (featureTables.length) {
placeFeatureTables(resultRef);
}
}, [mapView])
return (
<Tabs defaultActiveKey={featureTables[0].layer.title} className="tableResults">
{featureTables && featureTables.map((table, i) => {
return (
<Tab
className="TableTab"
title={`${table.layer.title.slice(0,10)}...`}
eventKey={table.layer.title}
key={`${i}`}
>
<div ref={resultRef.current[i]}></div>
</Tab>
)
})}
</Tabs>
)
};
And here's the code for the placeFeatureTables() function from an even higher component that the useEffect uses which updates the featureTables state with the container / Ref.
const placeFeatureTables = (containerRefs) => {
setFeatureTables(prev => {
const placedTables = prev.map((table, i) => {
table.container = containerRefs.current[i].current;
return table;
})
return placedTables;
})
}
I feel like this is most likely the issue area and the Containers / Refs are not being updated properly on re-render and I should be using something other than useRef. The thing that's really bugging me though, is that I swear this was working earlier.
Note1: The FeatureTables come from ESRI ArcGIS Javascript API. Although, it's just the Ref being passed, so I don't think it's necessary to know about them. FeatureTable Documentation
Note2: If I console.log the featureTables. The first time, the container highlights the area where it's at. Once I close and re-open and then click on the new featureTables container, it says the node does not exist.
Related
I'm looking for a way to render a react component (e.g. mui's Link) inside a popup created in react-leaflet's pointToLayer function of the GeoJSON component. Alternatively, invoke a function by clicking <a> or <button> in the popup.
The code I'm using now kind of works – I could redirect a user to a different URL. The problem is that I want the link inside the popup to call a function (which will change react's state, e.g. opening mui's Drawer).
import L from "leaflet";
import geoJSON from "../resources/test.json";
function callMe() {
console.log("test") //will change state
}
function featureToCircle(feature, latlng) {
const pointName = "from feature";
const popup = L.popup().setContent(`
<p>${pointName} – (${latlng.lat}, ${latlng.lng})</p>
invoke callMe()
`);
//ideally: <Link href="#" onClick={() => callMe() }>invoke callMe()</Link>
return L.circleMarker(latlng, {
color: "yellow",
})
.bindPopup(popup)
.bindTooltip(pointName);
}
export default function MyGeoJSON(props) {
return (
<GeoJSON key="whatever" data={geoJSON} pointToLayer={featureToCircle} />
);
}
I tried with ReactDOMServer.renderToString – unfortunately, the onClick doesn't work with this approach.
I'm also aware that I could replace GeoJSON with separate tags, like here, and it should work. I treat it as the last resort, though, and would rather use GeoJSON, to avoid replicating its functionality.
I've got a sample Resium project that shows the map. I've added a simple onClick that sets some state which is NOT being used anywhere - I just set it. Still, it causes the entire map to redraw & flicker UNLESS I remove the terrainProvider. Example (terrainProvider is commented out) if you move/click the mouse, the entire Cesium UI flickers and redraws everything. I'm using React 17, Resium 1.14.3 (Cesium 1.86.1). Any idea what's going on?
import React, { useState } from "react";
import { Cartesian3, Color } from "cesium";
import { Viewer, Entity } from "resium";
import * as Cesium from 'cesium';
export default function App() {
const [currentPos, setCurrentPos] = useState(null);
const handleMouseClick = (e, t) => {
setCurrentPos("xxx");
}
return (
<Viewer full
onClick={handleMouseClick}
onMouseMove={handleMouseClick}
// terrainProvider={new Cesium.CesiumTerrainProvider({ url: 'https://api.maptiler.com/tiles/terrain-quantized-mesh-v2/?key=xxxxxxx' })}
>
<Entity
name="Tokyo"
position={Cartesian3.fromDegrees(139.767052, 35.681167, 100)}
point={{ pixelSize: 10, color: Color.RED }}
/>
</Viewer>
);
}
My guess is this. No matters that the currentPos is not used in the component, is still part of the component's state, then the components re-renders.
In every render, the terrainProvider prop is receiving a new instance, so this causes that the entire Viewer re-renders too.
Maybe you can save your instance as state in the component and don't create a new one everytime that the component is re-render.
I have modal screen (using react-bootstrap), on modal screen i have multiple overlays (popup menus) linked to items. These overlays has inputs, and when i click on input it immediately loses focus. I cant figure out whats wrong, because another one popup menu, that i have on normal screen, not modal, works fine. Tried to set autofocus, but it immediately loses too.
I wrote example, https://codesandbox.io/s/rkemy
I think it is somehow connected with popper, because bootstrap overlay uses it, dont know where to dig
The fix provided in the other response is a workaround that doesn't fix the real cause of the issue.
The issue is caused by an internal logic of the Modal component of react-overlay library that is a dependency library of react-bootstrap.
Specifically, the issue is caused by code listed below
const handleEnforceFocus = useEventCallback(() => {
if (!enforceFocus || !isMounted() || !modal.isTopModal()) {
return;
}
const currentActiveElement = activeElement();
if (
modal.dialog &&
currentActiveElement &&
!contains(modal.dialog, currentActiveElement)
) {
modal.dialog.focus();
}
});
that enforce the focus on the first modal open as soon as that modal lose the focus, like when you move the focus on the input.
In order to solve the issue, you have to pass the enforceFocus={false} to your Modal component.
The documentation of the API can be found here: https://react-bootstrap.github.io/react-overlays/api/Modal#enforceFocus
As the docs says:
Generally this should never be set to false as it makes the Modal less accessible to assistive technologies, like screen readers. but in your scenario this is a need to work properly.
Solution is to wrap Overlay in container:
import React from "react";
import { Overlay } from "react-bootstrap";
import { X } from "react-bootstrap-icons";
export const PopupMenuWrapper = (props) => {
const { target, title, show, onClose, children } = props;
const ref = React.useRef(null);
return (
<div ref={ref}>
<Overlay
container = {ref.current}
target={target.current}
show={show}
placement="bottom-start"
rootClose={true}
onHide={onClose}
>
...
</div>
...
I'm trying to use scroll position for my animations in my web portfolio. Since this portfolio use nextJS I can't rely on the window object, plus I'm using navigation wide slider so I'm not actually scrolling in the window but in a layout component called Page.
import React, { useEffect } from 'react';
import './page.css';
const Page = ({ children }) => {
useEffect(() => {
const scrollX = document.getElementsByClassName('page')
const scrollElement = scrollX[0];
console.log(scrollX.length)
console.log(scrollX)
scrollElement.addEventListener("scroll", function () {
console.log(scrollX[0].scrollTop)
});
return () => {
scrollElement.removeEventListener("scroll", () => { console.log('listener removed') })
}
}, [])
return <div className="page">{children}</div>;
};
export default Page;
Here is a production build : https://next-portfolio-kwn0390ih.vercel.app/
At loading, there is only one Page component in DOM.
The behaviour is as follow :
first listener is added at first Page mount, when navigating, listener is also added along with a new Page component in DOM.
as long as you navigate between the two pages, no new listener/page is added
if navigating to a third page, listener is then removed when the old Page is dismounted and a new listener for the third page is added when third page is mounted (etc...)
Problem is : when you navigate from first to second, everything looks fine, but if you go back to the first page you'll notice the console is logging the scrollX value of the second listener instead of the first. Each time you go on the second page it seems to add another listener to the same scrollElement even though it's not the same Page component.
How can I do this ? I'm guessing the two component are trying to access the same scrollElement somewhat :/
Thanks for your time.
Cool site. We don't have complete info, but I suspect there's an issue with trying to use document.getElementsByClassName('page')[0]. When you go to page 2, the log for scrollX gives an HTMLCollection with 2 elements. So there's an issue with which one is being targeted. I would consider using a refs instead. Like this:
import React, { useEffect, useRef } from 'react';
import './page.css';
const Page = ({ children }) => {
const pageRef = useRef(null)
const scrollListener = () => {
console.log(pageRef.current.scrollTop)
}
useEffect(() => {
pageRef.addEventListener("scroll", scrollListener );
return () => {
pageRef.removeEventListener("scroll", scrollListener )
}
}, [])
return <div ref={pageRef}>{children}</div>;
};
export default Page;
This is a lot cleaner and I think will reduce confusion between components about what dom element is being referenced for each scroll listener. As far as the third page goes, your scrollX is still logging the same HTMLElement collection, with 2 elements. According to your pattern, there should be 3. (Though there should really only be 1!) So something is not rendering properly on page 3.
If we see more code, it might uncover the error as being something else. If refs dont solve it, can you post how Page is implemented in the larger scope of things?
also, remove "junior" from the "junior developer" title - you won't regret it
I'm experiencing some problems with scroll positions on the back button (history popstate) when using react-router. React router v4 doesn't handle scroll management out of the box because browsers are implementing some automatic scroll behavior. This is great except when the height of the browser window changes too dramatically from one view to another. I have implemented the ScrollToTop component as described here: https://reacttraining.com/react-router/web/guides/scroll-restoration
This works great. When you click a link and go to a different component, the browser scrolls to the top (like a normal server-rendered website would). The issue only happens when you go back (via the browser back button) to a view with a much taller window height. It seems that (chrome) tries to go to the scroll position of the previous page before react has rendered the content (and browser height). This results in the scroll only going as far down as it can based on the height of the view it's coming from. Picture this scenario:
View1: Long list of movies (window height 3500px).
(movie is clicked)
View2: Details view of the selected movie (window height: 1000px).
(Browser back button is clicked)
Back to view 1, but scroll position can't go further than 1000px, because chrome is trying to set the position before react renders the long movie list.
For some reason this is only a problem in Chrome. Firefox and Safari seem to handle it fine. I wonder if anyone else have had this problem, and how you guys generally handle scroll restoration in React.
Note: all the movies are imported from a sampleMovies.js — so I'm not waiting for an API response in my example.
Note that history.scrollRestoration is just a way of disabling the browser's automatic attempts at scroll restoration, which mostly don't work for single-page apps, so that they don't interfere with whatever the app wants to do. In addition to switching to manual scroll restoration, you need some sort of library that provides integration between the browser's history API, React's rendering, and the scroll position of the window and any scrollable block elements.
After not being able to find such a scroll restoration library for React Router 4, I created one called react-scroll-manager. It supports scrolling to top on navigation to a new location (aka history push) and scroll restoration on back/forward (aka history pop). In addition to scrolling the window, it can scroll any nested element that you wrap in an ElementScroller component. It also supports delayed/asynchronous rendering by using a MutationObserver to watch the window/element content up to a user-specified time limit. This delayed rendering support applies to scroll restoration as well as scrolling to a specific element using a hash link.
npm install react-scroll-manager
import React from 'react';
import { Router } from 'react-router-dom';
import { ScrollManager, WindowScroller, ElementScroller } from 'react-scroll-manager';
import { createBrowserHistory as createHistory } from 'history';
class App extends React.Component {
constructor() {
super();
this.history = createHistory();
}
render() {
return (
<ScrollManager history={this.history}>
<Router history={this.history}>
<WindowScroller>
<ElementScroller scrollKey="nav">
<div className="nav">
...
</div>
</ElementScroller>
<div className="content">
...
</div>
</WindowScroller>
</Router>
</ScrollManager>
);
}
}
Note that an HTML5 browser (10+ for IE) and React 16 are required. HTML5 provides the history API, and the library uses the modern Context and Ref APIs from React 16.
How do you handle your scroll restoration?
Turns out browsers have implementations of the history.scrollRestoration.
Maybe you can use that? Check these links out.
https://developer.mozilla.org/en/docs/Web/API/History#Specifications
https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration
In addition, I found an npm module that might be able to handle scroll restoration in react with ease, but this library only works with react router v3 and below
https://www.npmjs.com/package/react-router-restore-scroll
https://github.com/ryanflorence/react-router-restore-scroll
I hope this can help.
my solution:save window.scrollY for every pathname with a Map (ES6)
scroll-manager.tsx
import { FC, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export function debounce(fn: (...params: any) => void, wait: number): (...params: any) => void {
let timer: any = null;
return function(...params: any){
clearTimeout(timer);
timer = setTimeout(()=>{
fn(...params)
}, wait);
}
}
export const pathMap = new Map<string, number>();
const Index: FC = () => {
const { pathname } = useLocation();
useEffect(() => {
if (pathMap.has(pathname)) {
window.scrollTo(0, pathMap.get(pathname)!)
} else {
pathMap.set(pathname, 0);
window.scrollTo(0, 0);
}
}, [pathname]);
useEffect(() => {
const fn = debounce(() => {
pathMap.set(pathname, window.scrollY);
}, 200);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, [pathname]);
return null;
};
export default Index;
App.tsx
function App() {
return (
<Router>
<ScrollManager/>
<Switch>...</Switch>
</Router>
);
}
You can also use pathMap.size === 1 to determine if the user entered the app for the first time
I wound up using localStorage to track the scroll position - not sure this would handle all situations.
In this example, there's a Company page with a set of Stores, and each Store has a set of Display cases. I needed to track the scroll position of the display cases, so saved that to a 'storeScrollTop' key. There were 6 lines of code to add.
company.jsx:
// click on a store link
const handleClickStore = (evt) => {
window.localStorage.removeItem('storeScrollTop') // <-- reset scroll value
const storeId = evt.currentTarget.id
history.push(`/store/${storeId}`)
}
store.jsx:
// initialize store page
React.useEffect(() => {
// fetch displays
getStoreDisplays(storeId).then(objs => setObjs(objs)).then(() => {
// get the 'store' localstorage scrollvalue and scroll to it
const scrollTop = Number(window.localStorage.getItem('storeScrollTop') || '0')
const el = document.getElementsByClassName('contents')[0]
el.scrollTop = scrollTop
})
}, [storeId])
// click on a display link
const handleClickDisplay = (evt) => {
// save the scroll pos for return visit
const el = document.getElementsByClassName('contents')[0]
window.localStorage.setItem('storeScrollTop', String(el.scrollTop))
// goto the display
const displayId = evt.currentTarget.id
history.push(`/display/${displayId}`)
}
The trickiest part was figuring out which element had the correct scrollTop value - I had to inspect things in the console until I found it.
This component scroll to up if page is new but if page seen before restore the scroll.
scroll-to-top.tsx file:
import { useEffect, FC } from 'react';
import { useLocation } from 'react-router-dom';
const pathNameHistory = new Set<string>();
const Index: FC = (): null => {
const { pathname } = useLocation();
useEffect((): void => {
if (!pathNameHistory.has(pathname)) {
window.scrollTo(0, 0);
pathNameHistory.add(pathname);
}
}, [pathname]);
return null;
};
export default Index;
app.tsx file:
<BrowserRouter>
<ScrollToTop />
<App />
</BrowserRouter>
Use this library
https://www.npmjs.com/package/react-scroll-restoration
React Router does not provide out of the box support for scroll restoration and as it currently stands they won't either, because browsers are implementing some automatic scroll behavior