My problem: https://imgur.com/a/haBPnKB
When putting my viewState in Redux - and starting any kind of transition, I get a clear flicker, where the map will start moving, then return back to its starting position, before continuing the movement. I sometimes also get a warning like the one below when this happens:
[Violation] 'requestAnimationFrame' handler took 103ms
If this is some kind of a race condition, it is a very consistent one. The violation for requestAnimationFrame only happens sometimes.
Neither of these problems exist when I store the viewState in a "useState"-hook instead of Redux.
CodeSandbox: https://codesandbox.io/s/inspiring-cohen-li1zy
Whenever the onViewStateChange-event fires, I log out the current viewState, and the next one like this:
<DeckGL
layers={[]}
viewState={viewState}
controller={MapController}
onViewStateChange={e => {
console.table([viewState, e.viewState])
setViewState(e.viewState)
}}
>
<StaticMap reuseMaps mapStyle={mapStyle} />
</DeckGL>


I think the problem here could stem from how JavaScript (as a turn-based language) decides what to run next.
Essentially what we have here is a race condition that will only happen if "the turn" is given up in between:
Allowing the internal map handlers to run in between emitting a state and before receiving a new one can be accomplished with a setTimeout of 0ms.
export default function WorldMap() {
const [viewState, setViewState] = useState(initialViewState);
return (
<div>
<DeckGL
layers={[]}
viewState={viewState}
controller={MapController}
onViewStateChange={e => {
setTimeout(() => setViewState(e.viewState), 0);
}}
>
<StaticMap
reuseMaps
mapboxApiAccessToken={mapboxApiAccessToken}
mapStyle={mapStyle}
/>
</DeckGL>
</div>
);
}
This completely messes up transitions. Way worse than the example using Redux.
If the map never executes the onViewStateChange handler without first having received the latest viewState, then I believe that should fix the problem.
This will prevent multiple viewState's from being out in the feedback loop at the same time. (which based on the log output in the Redux case seems to be causing the flicker)
Might be related to this issue: https://github.com/reduxjs/react-redux/issues/1298
deck immediately dispatches an onViewStateChange when a new view state with transition is set. This is not an issue in non-React use cases, but React/Redux place a delay in between the update and the render.
One possible solution is to block the destination view state from rendering when the transition is first triggered. This will create a discrepancy between the React props and the camera for 1 frame, i.e. if you have other UI that updates to the longitude/latitude in the Redux store, they will go out of sync with the map during this frame.
Another solution is to add a transitionTo method on the DeckGL component. This will trigger onViewStateChange from the correct first frame.
Okay, so the transitionTo-method would mean that the first event here is never emitted?

My first impression based on what you are saying is that the transitionTo-method sounds like the better solution.
You are currently triggering the transition via an update to the Redux store:
-> redux store viewState updates with destination coordinates
-> DeckGL rerenders with new viewState
-> onViewStateChange is called
-> redux store viewState updates with intermediate coordinates
...
In the second proposal, you'll need to call the transitionTo API:
-> call DeckGL's ref.current.transitionTo(viewState)
-> onViewStateChange is called
-> redux store viewState updates with intermediate coordinates
To avoid having to use refs, and to make the style a bit more declarative - what about doing something like:
export default function WorldMap() {
const [viewState, setViewState] = useState(initialViewState);
const [transitionTo, setTransitionTo] = useState({})
useEffect(() => {
setTransitionTo({
inTransition: true,
// only the part of the viewState that is going to change needs to be specified here
targetViewState: { longitude: 10, latitude: 62, zoom: 9 },
transitionInterpolator: new LinearInterpolator(),
durationLeft: 3000, // ms
// alternatively, a speed could be specified here
})
}, [])
return (
<div>
<DeckGL
layers={[]}
controller={MapController}
// This keeps track of "where you are" at all times
// viewState doesn't know anything about transitions
viewState={viewState}
onViewStateChange={e => setViewState(e.viewState)}
// This keeps track of "where you are headed"
// Kind of like you can set a destination on your self-driving car, and have it take you there
// But then you could always change your mind half way to the destination
transitionTo={transitionTo}
onTransitionToChange={e => setTransitionTo(e.transitionTo)}
>
<StaticMap
reuseMaps
mapboxApiAccessToken={mapboxApiAccessToken}
mapStyle={mapStyle}
/>
</DeckGL>
</div>
);
}
Obviously this would be a rather significant API change - which might not be ideal for a library that is used by a lot of people.
I'm guessing the transition should just continue (from wherever the new viewState was placed). If this is farther away from the target, then the transition might have to speed up.
An advantage of this is that the transition-object and current viewState would be enough to describe how to continue the transition, and if it has finished. The transitions would be "stateless". (At least from Reacts perspective)
A problem here is that a feedback loop with a finite delay can mean that the transition takes longer than anticipated, or even cause a race. Instead, storing the absolute timestamp of completion could be more robust. (using performance.now())
// What is used now
{
transitionDuration: 3000
}
// pros: Easy to understand
// cons: this can't be used to describe intermediate states, forcing Deck.GL to be more stateful
// duration proposal 1:
{
transitionDurationLeft: 3000
}
// pros: Describes the current state of the transition, allowing Deck.GL to be more stateless
// cons: Can cause data races/could take longer than the specified time
// duration proposal 2:
{
transitionCompletesAt: performance.now() + 3000
}
// pros: Avoids data race in case of delays (since it is a constant value), describes current state of transition.
// cons: looks a bit more noisy. Can't as easily pause transition.
It could make sense to have them be combined as a single object, sharing a single event handler.
I guess an advantage of your first proposal is that it won't really change how the API is used from the outside? Meaning people wouldn't have to change their code. Is this correct?
The trade-off here being the discrepancy between props and camera for one frame as you mentioned.
In the idea above, as well as your second proposal - people would need to modify their code.
I ended up creating this as a workaround: https://github.com/bergkvist/react-context-toolkit
React Context API doesn't seem to have zero delays between a dispatch and a rerender. (in contrast to react-redux), which means the flicker doesn't happen.
We use react context to manage this. Be wary of combining state and state updaters in one consumable hook to avoid constant re-renders.
// ViewportContext.js
import React from 'react';
const ViewportContext = React.createContext();
const SetViewportContext = React.createContext();
/* Wrap your app in this bad boy */
export const ViewportProvider = ({defaultViewport = {}, children}) => {
const [viewport, setViewport] = useState(defaultViewport);
return (
<ViewportContext.Provider value={viewport}>
<SetViewportContext.Provider value={setViewport}>
{children}
</SetViewportContext.Provider>
</ViewportContext.Provider>
)
}
/** Read the viewport from anywhere */
export const useViewport = () => {
const ctx = useContext(ViewportContext);
if (!ctx) throw Error("Not wrapped in <ViewportProvider />.")
return ctx;
}
/** Update the viewport from anywhere */
export const useSetViewport = () => {
const ctx = useContext(SetViewportContext);
if (!ctx) throw Error("Not wrapped in <ViewportProvider />.")
return ctx;
}
// attaching to deck
import
export const MyMap = () => {
const viewport = useViewport();
const setViewport = useSetViewport();
return <Deck
viewState={viewport}
onViewStateChange={({viewState}) => setViewport(viewState)}
/>
}