deck.gl: [Bug] React 18 compatibility
Description
I’ve been working with deck.gl + react-map-gl + React 17 for one year, almost every days, and I always used <DeckGL />
as a stateless component, following the doc : https://deck.gl/docs/developer-guide/interactivity#externally-manage-view-state.
Geolocation and camera orientation are controlled from React, viewState
is set with React useState()
or Jotai atom.
Everything worked fine until I tried to use the exact same code with React 18. When the state is controlled with viewState
+ onViewStateChange()
weird things happen. But using <DeckGL />
with the initialViewState
works fine and smoothly.
For some time I thought it worked fine when I removed react-map-gl <Map />
but I was wrong, the bug was there without rendering it. It’s easier to check this in Safari. Rendering <Map/>
seems to amplify the problem.
I spent some time reading issues and documentations from deck.gl and react-map-gl, and I did a lot of searches on google, but I found nothing about this problem, and nothing that helped me solve it.
Observations with React 18 defaults
- in chromium-based browsers, scrolling to zoom in/out is possible but sometimes the movement ends with bounces. It looks like spring animation.
- in Safari, scrolling to zoom in/out is almost impossible, the camera moves a little and comes back almost at the starting point.
- in Firefox it’s more complicated, from yesterday until this morning it was like Safari but it’s now almost perfect. 😵 The only rational explanation I can think of would be an an auto-update of the browser I would have missed.
I tried to do some testing on browserstack but the rendering is too slow to have a good feedback, sorry.
Attempts to solve the problem
After 2 days looking for the origin of the problem, automatic batching in React 18 could be responsible of this. I’m not in my confort zone at all, so I could be totally wrong.
I tried to dig for deeper understanding, and I discovered the flushSync()
function from react-dom to disable batch rendering. I wrapped setViewState()
with it in onViewStateChange()
, it’s better, deck.gl layer is zoomed smoothly but deck.gl and react-map-gl are desynchronized, basemap update is lagging behind.
And there is a warning from React saying : Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.
Doing more research on microtasks I tried to wrap the setViewState()
in queueMicrotask()
or requestAnimationFrame()
instead of flushSync()
.
The result was better with requestAnimationFrame()
but it’s not a solution : it’s laggy, some frames are sometimes dropped, but scrolling is possible in all browsers, bounces are gone in chrome and deck.gl layer stay synchronized with react-map-gl basemap. That is about as far as I can go by myself.
I hope to have forgotten nothing and I wish I’m sufficiently clear on this problem, english is not my native language.
I take this opportunity to thank you for the amazing vis.gl libraries and all your hard work !
Flavors
- React
- Python/Jupyter notebook
- MapboxLayer
- GoogleMapsOverlay
- CartoLayer
- DeckLayer/DeckRenderer for ArcGIS
Expected Behavior
Zoom in/out should be smooth, without bounces, like it was with React 17.
Steps to Reproduce
https://codesandbox.io/s/deck-gl-v8-8-react-v18-mwbyru?file=/src/App.js Fullscreen : https://mwbyru.csb.app/ (I think I saw more problems in fullscreen than in editor view)
A <select />
allows to use different “render modes”:
- default
- setState() wrapped in flushSync()
- setState() wrapped in requestAnimationFrame()
A checkbox allows to enable/disable the react-map-gl basemap.
Environment
- Framework version: deck.gl@8.8.4
- Other libraries :react-map-gl@7.0.17 + react@18.2 + maplibre-gl@2.1.9
- Browser: Chrome Version 103.0.5060.114 (Build officiel) (x86_64), Safari Version 15.5 (16613.2.7.1.9, 16613), Firefox 102.0.1 (64 bits)
- OS: MacOS Big Sur 11.6.6
Logs
No response
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 7
- Comments: 18 (1 by maintainers)
I do notice touch pad zooming performance is much better on React 18 when mounting the app createRoot instead of render. However when I use createRoot, the canvas layers will flicker on window resize or canvas width resize.
I didn’t have any time to work on this problem until now. Thanks for the different answers, sadly nothing helped me solve this problem. I updated my mac to macOs 13 Venturi, but the problem is always there with Safari 16.1.
Today I tried to better understand how the “wheel” event was handled, because my last try showed a clear difference between mouse wheel event and trackpad wheel event on Safari. The mouse wheel worked, whereas the trackpad event reacted like a spring animation and the zoom came back to its initial value. Logging
viewState
showed that zoom values often came back to the exact same value after a bounce, and, sometimes, changed a little allowing a small zoom.I tried different settings in the
scrollZoom
property of thecontroller
prop : different speeds andsmooth : true
, but no miracle here. I just noticed it was worse withsmooth : true
.After diving into codebase, I found the
_onWheel
function in controller.ts : https://github.com/visgl/deck.gl/blob/83ce5c11db4f577b93677ca960e95c27985631d8/modules/core/src/controllers/controller.ts#L530 In this function there is a call to_getTransitionProps
(line 553). As the problem looked like a spring animation I tried to find a way to avoid the transition. TheCommonViewState
type equalsTransitionProps
, so I tried to settransitionDuration: 0
in theviewState
object and then… the zoom worked smoothly with the trackpad wheel event ! 🤩 It seems to be the best result in all my tests.I updated the sandbox with this last solution : https://codesandbox.io/s/deck-gl-v8-8-react-v18-mwbyru?file=/src/App.js
https://user-images.githubusercontent.com/16524008/204373886-90188ecc-9ba6-456d-84dc-c96d5c3c7877.mp4
I fear some possible side effects I’m not aware of, and I wonder if there is a better solution : a way to set this transition to 0 by default ? or only during ‘wheel’ event ? @Pessimistress I’d love to have advices on this subject.