Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgperry committed Mar 9, 2021
1 parent ed7ea87 commit d07f81a
Show file tree
Hide file tree
Showing 22 changed files with 404 additions and 204 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@typescript-eslint/class-name-casing": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"react/jsx-pascal-case": "error",
"react-hooks/rules-of-hooks": "error",
// "react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"import/no-default-export": "error",
"prefer-arrow-callback": "warn",
Expand Down
2 changes: 1 addition & 1 deletion src/motion/context/MotionConfigContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface MotionConfigProps extends Partial<MotionConfigContext> {
export const MotionConfigContext = createContext<MotionConfigContext>({
transformPagePoint: (p) => p,
features: [],
isStatic: false,
isStatic: typeof window === "undefined",
})

/**
Expand Down
60 changes: 57 additions & 3 deletions src/motion/context/MotionContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,62 @@
import { createContext, useContext } from "react"
import { createContext, useContext, useMemo } from "react"
import { VisualElement } from "../../render/types"
import {
checkIfControllingVariants,
isVariantLabel,
} from "../../render/utils/variants"
import { MotionProps } from "../types"

export const MotionContext = createContext<VisualElement | undefined>(undefined)
export interface MotionContextProps {
visualElement?: VisualElement
initial?: false | string | string[]
animate?: string | string[]
}

export const MotionContext = createContext<MotionContextProps>({})

export function useVisualElementContext() {
return useContext(MotionContext)
return useContext(MotionContext).visualElement
}

export function getCurrentTreeVariants(
props: MotionProps,
context: MotionContextProps
): MotionContextProps {
if (checkIfControllingVariants(props)) {
const { initial, animate } = props
return {
initial:
initial === false || isVariantLabel(initial)
? (initial as any)
: undefined,
animate: isVariantLabel(animate) ? animate : undefined,
}
}
return props.inherit !== false ? context : {}
}

function variantLabelsAsDependency(
prop: undefined | string | string[] | boolean
) {
return Array.isArray(prop) ? prop.join(" ") : prop
}

export function useCreateMotionContext(
props: MotionProps,
isStatic: boolean
): MotionContextProps {
const { initial, animate } = getCurrentTreeVariants(
props,
useContext(MotionContext)
)

return useMemo(
() => ({ initial, animate }),
isStatic
? [
variantLabelsAsDependency(initial),
variantLabelsAsDependency(animate),
]
: []
)
}
75 changes: 75 additions & 0 deletions src/motion/context/__tests__/MotionContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { animationControls } from "../../../animation/animation-controls"
import { getCurrentTreeVariants } from "../MotionContext"

describe("getCurrentTreeVariants", () => {
test("It returns the correct variant to render currently", () => {
expect(
getCurrentTreeVariants(
{
initial: { opacity: 0 },
animate: { opacity: 1 },
},
{
initial: "a",
animate: "b",
}
)
).toEqual({ initial: "a", animate: "b" })

expect(
getCurrentTreeVariants(
{
initial: { opacity: 0 },
animate: { opacity: 1 },
inherit: false,
},
{
initial: "a",
animate: "b",
}
)
).toEqual({ initial: undefined, animate: undefined })

expect(
getCurrentTreeVariants(
{
initial: false,
animate: ["a", "b"],
},
{
initial: "c",
animate: "b",
}
)
).toEqual({ initial: false, animate: ["a", "b"] })

expect(
getCurrentTreeVariants(
{
initial: ["c", "d"],
animate: ["a", "b"],
},
{
initial: ["e", "f"],
animate: ["g", "h"],
}
)
).toEqual({
initial: ["c", "d"],
animate: ["a", "b"],
})

expect(
getCurrentTreeVariants(
{
initial: false,
animate: animationControls(),
},
{
initial: "a",
animate: "b",
}
)
).toEqual({ initial: false, animate: undefined })
})
})
2 changes: 1 addition & 1 deletion src/motion/features/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export interface MotionFeature {

export type RenderComponent = (
props: MotionProps,
visualElement: VisualElement
visualElement?: VisualElement
) => any
5 changes: 0 additions & 5 deletions src/motion/features/use-features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,11 @@ import { MotionFeature } from "./types"
*/
export function useFeatures(
defaultFeatures: MotionFeature[],
isStatic: boolean,
visualElement: VisualElement,
props: MotionProps
): null | JSX.Element[] {
const plugins = useContext(MotionConfigContext)

// If this is a static component, or we're rendering on the server, we don't load
// any feature components
if (isStatic || typeof window === "undefined") return null

const allFeatures = [...defaultFeatures, ...plugins.features]
const numFeatures = allFeatures.length
const features: JSX.Element[] = []
Expand Down
79 changes: 51 additions & 28 deletions src/motion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { MotionProps } from "./types"
import { RenderComponent, MotionFeature } from "./features/types"
import { useFeatures } from "./features/use-features"
import { MotionConfigContext } from "./context/MotionConfigContext"
import { MotionContext } from "./context/MotionContext"
import { CreateVisualElement } from "../render/types"
import { MotionContext, useCreateMotionContext } from "./context/MotionContext"
import { CreateVisualElement, VisualElement } from "../render/types"
import { useVisualElement } from "./utils/use-visual-element"
import { useCreateVisualState } from "./utils/use-create-visual-state"
export { MotionProps }

export interface MotionComponentConfig<E> {
Expand All @@ -33,44 +34,66 @@ export function createMotionComponent<P extends {}, E>({
}: MotionComponentConfig<E>) {
function MotionComponent(props: P & MotionProps, externalRef?: Ref<E>) {
/**
* If a component is static, we only visually update it as a
* result of a React re-render, rather than any interactions or animations.
* If this component or any ancestor is static, we disable hardware acceleration
* and don't load any additional functionality.
* If we're rendering in a static environment, we only visually update the component
* as a result of a React-rerender rather than interactions or animations. This
* means we don't need to load additional memory structures like VisualElement,
* or any gesture/animation features.
*/
const { isStatic } = useContext(MotionConfigContext)

/**
* Create a VisualElement for this component. A VisualElement provides a common
* interface to renderer-specific APIs (ie DOM/Three.js etc) as well as
* providing a way of rendering to these APIs outside of the React render loop
* for more performant animations and interactions
* We don't hyrdrate the VisualElement in static contexts.
*/
const visualElement = useVisualElement(
createVisualElement,
props,
isStatic,
externalRef
)
let visualElement: VisualElement | undefined = undefined
let features: JSX.Element[] | null = null

/**
* Load features as renderless components unless the component isStatic
* Create the tree context. This is memoized and will only trigger renders
* when the current tree variant changes in static mode.
*/
const features = useFeatures(
defaultFeatures,
isStatic,
visualElement,
props
)
const context = useCreateMotionContext(props, isStatic)

/**
*
*/
const visualState = useCreateVisualState(props, context, isStatic)

const component = useRender(props, visualElement)
if (!isStatic) {
/**
* Create a VisualElement for this component. A VisualElement provides a common
* interface to renderer-specific APIs (ie DOM/Three.js etc) as well as
* providing a way of rendering to these APIs outside of the React render loop
* for more performant animations and interactions
*/
visualElement = useVisualElement(
createVisualElement,
props,
externalRef
)

// The mount order and hierarchy is specific to ensure our element ref is hydrated by the time
// all plugins and features has to execute.
/**
* Load Motion gesture and animation features. These are rendered as renderless
* components so each feature can optionally make use of React lifecycle methods.
*
* TODO: The intention is to move these away from a React-centric to a
* VisualElement-centric lifecycle scheme.
*/
features = useFeatures(defaultFeatures, visualElement, props)

/**
* This can be mutative because visualElement will never change between re-renders.
*/
context.visualElement = visualElement
}

/**
* The mount order and hierarchy is specific to ensure our element ref
* is hydrated by the time features fire their effects.
*/
return (
<>
<MotionContext.Provider value={visualElement}>
{component}
<MotionContext.Provider value={context}>
{useRender(props, visualElement)}
</MotionContext.Provider>
{features}
</>
Expand Down
83 changes: 83 additions & 0 deletions src/motion/utils/use-create-visual-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useContext, useMemo } from "react"
import { isAnimationControls } from "../../animation/animation-controls"
import { PresenceContext } from "../../components/AnimatePresence/PresenceContext"
import { ResolvedValues } from "../../render/types"
import {
checkIfControllingVariants,
resolveVariantFromProps,
} from "../../render/utils/variants"
import { isMotionValue } from "../../value/utils/is-motion-value"
import { MotionContextProps } from "../context/MotionContext"
import { MotionProps } from "../types"
import { isForcedMotionValue } from "./is-forced-motion-value"

/**
*
*/
export function useCreateVisualState(
props: MotionProps,
context: MotionContextProps,
isStatic: boolean
): ResolvedValues {
const createVisualState = () => {
const values: ResolvedValues = {}
const presenceContext = useContext(PresenceContext)
const blockInitialAnimation = presenceContext?.initial === false

/**
* TODO: Make this renderer specific using the scrapMotionProps
*
* const motionValues = scrapeMotionValues()
*
*/
const { style } = props
for (const key in style) {
if (isMotionValue(style[key])) {
values[key] = style[key].get()
} else if (isForcedMotionValue(key, props)) {
values[key] = style[key]
}
}

let { initial, animate } = props
const isControllingVariants = checkIfControllingVariants(props)
const isVariantNode = isControllingVariants || props.variants

if (
context &&
isVariantNode &&
!isControllingVariants &&
props.inherit !== false
) {
initial ??= context.initial
animate ??= context.animate
}

const variantToSet =
blockInitialAnimation || initial === false ? animate : initial

if (
variantToSet &&
typeof variantToSet !== "boolean" &&
!isAnimationControls(variantToSet)
) {
const list = Array.isArray(variantToSet)
? variantToSet
: [variantToSet]
list.forEach((definition) => {
const resolved = resolveVariantFromProps(props, definition)
if (!resolved) return

const { transitionEnd, transition, ...target } = resolved

for (const key in target) values[key] = target[key]
for (const key in transitionEnd)
values[key] = transitionEnd[key]
})
}

return values
}

return isStatic ? createVisualState() : useMemo(createVisualState, [])
}
Loading

0 comments on commit d07f81a

Please sign in to comment.