Skip to content

Commit

Permalink
Merge pull request #2198 from framer/fix/use-instant-transition
Browse files Browse the repository at this point in the history
Improve useInstantTransition
  • Loading branch information
mergetron[bot] authored Jun 23, 2023
2 parents 6114276 + 7e8f31b commit 90e5b1c
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as React from "react"
import { useInstantTransition } from "../use-instant-transition"
import { useEffect } from "react"
import { act } from "@testing-library/react"
import { renderHook } from "@testing-library/react"
import { instantAnimationState } from "../use-instant-transition-state"

describe("useInstantTransition", () => {
test("Disables animations for a single render", async () => {
Expand Down Expand Up @@ -147,4 +149,48 @@ describe("useInstantTransition", () => {

expect(values).not.toEqual([100, 200, 400])
})

test("transitions stay blocked when called on multiple frames back-to-back", async () => {
const { result } = renderHook(() => useInstantTransition())

act(() => result.current(() => {}))
expect(instantAnimationState.current).toBe(true)

const promise = createResolvablePromise()

// On the next frame, call the callback again.
requestAnimationFrame(() => {
act(() => result.current(() => {}))

requestAnimationFrame(() => {
// If we hadn't called the callback a second time, we would have expected this to be `false` on this frame.
expect(instantAnimationState.current).toBe(true)
requestAnimationFrame(() => {
// Finally 2 frames have passed since the final call to
// start an instant transition, so we expect the state to be
// unblocked.
expect(instantAnimationState.current).toBe(false)
promise.resolve()
})
})
})

await promise
})
})

type ResolvablePromise<T = void> = Promise<T> & {
resolve: (value: T) => void
reject: (reason?: unknown) => void
}

function createResolvablePromise<T = void>(): ResolvablePromise<T> {
let resolvePromise: any, rejectPromise: any
const promise: any = new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
promise.resolve = resolvePromise
promise.reject = rejectPromise
return promise
}
16 changes: 14 additions & 2 deletions packages/framer-motion/src/utils/use-instant-transition.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import { frame } from "../frameloop"
import { useEffect } from "react"
import { useEffect, useRef } from "react"
import { useInstantLayoutTransition } from "../projection/use-instant-layout-transition"
import { useForceUpdate } from "./use-force-update"
import { instantAnimationState } from "./use-instant-transition-state"

export function useInstantTransition() {
const [forceUpdate, forcedRenderCount] = useForceUpdate()
const startInstantLayoutTransition = useInstantLayoutTransition()
const unlockOnFrameRef = useRef<number>()

useEffect(() => {
/**
* Unblock after two animation frames, otherwise this will unblock too soon.
*/
frame.postRender(() =>
frame.postRender(() => (instantAnimationState.current = false))
frame.postRender(() => {
/**
* If the callback has been called again after the effect
* triggered this 2 frame delay, don't unblock animations. This
* prevents the previous effect from unblocking the current
* instant transition too soon. This becomes more likely when
* used in conjunction with React.startTransition().
*/
if (forcedRenderCount !== unlockOnFrameRef.current) return
instantAnimationState.current = false
})
)
}, [forcedRenderCount])

Expand All @@ -22,6 +33,7 @@ export function useInstantTransition() {
instantAnimationState.current = true
forceUpdate()
callback()
unlockOnFrameRef.current = forcedRenderCount + 1
})
}
}

0 comments on commit 90e5b1c

Please sign in to comment.