ค้นหาเว็บไซต์

javascript - react-spring วิธีใช้ animated(Component)


ฉันกำลังพยายามแก้ไขปัญหาด้านประสิทธิภาพในตัวสไลเดอร์ภาพ และฉันได้พบว่าการใช้ animated.img ให้ประสิทธิภาพที่ดีกว่าการใช้ animated.div ที่มีส่วนประกอบของ React อยู่ภายใน

คอมโพเนนต์ของ React เห็นได้ชัดว่าไม่ได้วางไว้เฉยๆ เพื่อความสนุก แต่โชคดีที่ react-spring ให้คุณสร้างแอนิเมชันกับคอมโพเนนต์แบบกำหนดเองได้โดยการทำ

const AnimatedComponent = animated(Component)

ตามเอกสาร

แต่ฉันจะใช้มันยังไง? ฉันพยายามแล้ว แต่ TypeScript แค่ให้ข้อความที่ไม่ช่วยอะไรเกี่ยวกับ props ที่หายไปถึง 269 ประเภท.

แก้ไข เพิ่มข้อผิดพลาด

ข้อผิดพลาดของ TypeScript แสดงโดย VSCode แต่บางทีอาจไม่สำคัญ เนื่องจากฉันไม่รู้ว่าจะส่ง props อะไรเพื่อทำให้คอมโพเนนต์เคลื่อนไหว ฉันจึงไม่แปลกใจที่มันไม่ทำงาน แต่ข้อความข้อผิดพลาดนั้นก็ไม่ได้ช่วยในการระบุว่าฉันต้องทำอะไร

' is missing the following properties from type 'AnimatedProps<{ title: string | FluidValue<string, any>; id: string | FluidValue<string, any>; article?: { title: string; metaTitle: string; metaDescription: string; description: string; showRelatedArticles: boolean; elements: ({ ...; } | ... 4 more ... | { ...; })[]; } | null | undefined; ... 269 more ...; key?: st...': title, id, slot, animate, and 257 more.ts(2740)

ฉันได้ลบพร็อพบางส่วนของครั้งแรกออกไปเพราะฉันจำพวกมันจากคอมโพเนนต์ที่ฉันกำลังพยายามทำให้เคลื่อนไหวได้ และฉันรู้ว่าพวกมันมีอยู่

มีใครลองใช้สิ่งนี้แล้วหรือยัง? ตัวอย่างการใช้งานมันจะดีมาก

ฉันใช้เวอร์ชัน 9.0.0-rc.3 ของ react-spring ถ้านั่นสำคัญ

คำตอบ #1

พื้นหลัง

react-spring ไม่พึ่งพาการจับเวลา เหมือนกับ CSS transition API แต่จะทำการเปลี่ยนแปลงและแอนิเมชันจากมุมมองทางฟิสิกส์ เพื่อให้ได้ประสิทธิภาพที่ยอมรับได้ใน React มันจะข้าม React และทำการแก้ไขไปยังโหนด DOM ที่เกี่ยวข้องโดยตรง

คอมโพเนนต์แอนิเมชัน react-spring ปกติ

อย่างที่คุณอาจเห็น โหนด DOM ปกติทั้งหมดมีอยู่เป็นค่าที่เทียบเท่าของ react-spring ตัวอย่างเช่น animation.span, animation.div เป็นต้น... เหล่านี้ครอบองค์ประกอบ DOM พื้นฐานด้วยฟังก์ชันที่จำเป็นเพื่อให้ react-spring ทำงาน สิ่งที่ควรสังเกตที่นี่คือความละเอียดสองประการนี้:

  • ฟังก์ชันของ react-spring ถูกแนบเข้ากับโหนด DOM เดียว
  • เนื่องจากฟังก์ชันนี้ถูกแนบกับโหนด DOM พื้นฐาน จึงใช้เฉพาะพร็อพขององค์ประกอบ DOM พื้นฐานเท่านั้น

ข้อเท็จจริงทั้งสองนี้มีนัยสำหรับวิธีที่เราสามารถใช้คอมโพเนนต์ที่กำหนดเองซึ่งถูกห่อด้วย animated

คอมโพเนนต์ที่กำหนดเองของเรา

มาทำงานกับสถานการณ์ง่าย ๆ โดยใช้คอมโพเนนต์ฟังก์ชันของ React และ Typescript และดูว่าคุณสามารถแปลงมันเป็นคอมโพเนนต์ react-spring แบบกำหนดเองได้อย่างไร

สมมติว่าคุณมี div ที่คุณต้องการให้พื้นหลังเปลี่ยนสีเมื่อเปลี่ยนจากสีหนึ่งไปยังอีกสีหนึ่งหลังจากคลิกมัน

1. ไม่มี react-spring

วิธีการพื้นฐานจะเป็นดังนี้:

const Comp: FC = () => {

    const [color, setColor] = useState<string>("green")

    return (
        <div
            style={{ 
                backgroundColor: color,
                transition: "background-color 1s"
            }}
            onClick={ () => setColor(color => color === "blue" ? "green" : "blue") }
        />
    )
}
2. การใช้งานพื้นฐานของ react-spring

การทำสิ่งเดียวกันกับ react-spring การใช้งานพื้นฐานของ useSpring จะได้ผลลัพธ์เป็น

const Comp: FC = () => {
    
    const [color, setColor] = useState<string>("green")
    const springColor = useSpring({ backgroundColor: color})

    return (
        <animated.div
            style={springColor}
            onClick={ () => setColor(color => color === "blue" ? "green" : "blue") }
        />
    )
}
3. การใช้งาน react-spring ตามแนวทางปฏิบัติที่ดีที่สุด

ที่ดียิ่งกว่าคือการใช้ฟังก์ชันของ API เพื่อที่เราจะไม่ต้องเรนเดอร์คอมโพเนนต์ซ้ำทุกครั้งที่มีการเปลี่ยนสี เพื่อให้ชัดเจน เมื่อคุณใช้วิธีนี้ คุณจะไม่ได้เปลี่ยนแปลง prop ใด ๆ ที่ส่งไปยังคอมโพเนนต์ที่คุณต้องการอนิเมต ดังนั้นคุณสามารถเปลี่ยนสถานะของมันผ่าน API โดยไม่ต้องเรนเดอร์ซ้ำ ตราบใดที่ Comp เองไม่ถูกเรนเดอร์ซ้ำ

const Comp: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }))

    return (
        <animated.div
            style={springColor}
            onClick={ () => api.start({ backgroundColor: springColor.backgroundColor.goal === "blue" ? "green" : "blue" })}
        />
    )
}
4. ด้วยการมอบหมายให้กับชั้นเรียน

ลองคิดดู คุณกำลังส่งคุณสมบัติบางอย่างที่ห่อหุ้มแล้วไปยังคอมโพเนนต์ animated เหล่านี้ คุณสมบัติเหล่านี้มีประเภท SpringValue<T> และสามารถสร้างขึ้นได้โดยใช้ new หรือผ่านตัวอย่างเช่น useSpring ขั้นตอนแรกของเราสำหรับการสร้างคอมโพเนนต์แบบกำหนดเองคือเพียงแค่ส่งคุณสมบัติเหล่านี้เป็น property ให้กับคอมโพเนนต์ที่มีคอมโพเนนต์ animated อยู่ภายใน:

export interface CompProps {
    color: SpringValue<string>;
    onChangeColor: () => void;
}

const Comp: FC<CompProps> = (props: CompProps) => {

    return (
        <animated.div
            style={{ backgroundColor: props.color }}
            onClick={props.onChangeColor}
        />
    )
}

const Parent: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }));

    return (
        <Comp
            color={springColor.backgroundColor}
            onChangeColor={() => api.start({
                backgroundColor: springColor.backgroundColor.goal === "blue" ? "green" : "blue"
            })}
        />
    )
}
5. กับคอมโพเนนต์กำหนดเองที่เรียบง่าย ถูกห่อด้วย animated

ตอนนี้เราพร้อมที่จะทำการแทนที่และห่อหุ้ม property ของเราใน animated แทนการใช้ element animated ของ native ภายใน component ของเรา

export interface CompProps {
    style: CSSProperties;
    onChangeColor: () => void;
}

const Comp: FC<CompProps> = (props: CompProps) => {

    return (
        <div
            style={props.style}
            onClick={props.onChangeColor}
        />
    )
}

const WrappedComp: AnimatedComponent<FC<CompProps>> =
    animated(Comp)

const Parent: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }));

    return (
        <WrappedComp
            style={{ backgroundColor: springColor.backgroundColor }}
             onChangeColor={() =>
                 api.start({
                     backgroundColor:
                         springColor.backgroundColor.goal === "blue" ? "green" : "blue"
                  })
             }
         />
    )
}

โปรดทราบว่า ขณะนี้คอมโพเนนต์ที่เราแรปดูเหมือนคอมโพเนนต์ปกติและไม่มีสัญญาณใด ๆ ว่ากำลังถูกใช้ร่วมกับ react-spring แต่อย่างไรก็ตาม ตามที่เราจะเห็น ยังมีข้อกำหนดเพิ่มเติมบางอย่างสำหรับมันเพื่อให้การรวมกับ react-spring ทำงานได้ตามที่ตั้งใจ โปรดสังเกตว่าเราไม่ได้ส่ง backgroundColor เป็น prop อีกต่อไป แต่เราใช้ style แทน นอกจากนี้ prop ที่แอนิเมตทั้งหมดที่ส่งไปยังคอมโพเนนต์ที่กำหนดเองของเราคือ prop ที่สามารถใช้ได้กับองค์ประกอบ div พื้นฐานที่คอมโพเนนต์ที่กำหนดเองของเราผูกกับ forwarded ref ของมัน เพิ่มเติมเกี่ยวกับเรื่องนี้จะมีต่อไปด้านล่าง

ส่วนประกอบข้างต้นเป็นแบบง่าย ๆ เพราะว่า animated wrapper ไม่สามารถอัปเดตส่วนประกอบที่ห่อไว้โดยไม่ต้อง rerender ได้ ทำไมล่ะ? ง่าย ๆ เพราะส่วนประกอบที่เราห่อไว้นั้นไม่รองรับ ref ดังนั้นการเรียกร้องจาก react-spring ให้สามารถอัปเดตส่วนประกอบของเราโดยไม่ต้อง rerender จึงไม่สมเหตุสมผล

6. ด้วยคอมโพเนนต์กำหนดเองที่ถูกต้องซึ่งถูกห่อด้วย animated

มาลองพัฒนาคอมโพเนนต์ที่ห่อหุ้มของเราโดยให้มันรับ ref ได้

const Comp: FC<CompProps & RefAttributes<HTMLDivElement>> = 
    forwardRef<HTMLDivElement,CompProps>(
        (props, ref) => {

            return (
                <div 
                    ref={ref} 
                    style={props.style} 
                    onClick={props.onChangeColor} 
                />
            )
        }
    )

ตอนนี้ react-spring จะรับรู้ว่าคอมโพเนนต์ที่เราห่อรองรับ ref ดังนั้นมันจะงดการ rerender ทุกครั้งที่เกิดการเปลี่ยนแปลงและจะใช้ ref เพื่ออัปเดตแทน เนื่องจากการอัปเดตเกิดขึ้นผ่าน ref จึงสำคัญที่ props จะต้องเป็น props จริงบนองค์ประกอบที่เราต้องการเปลี่ยน; หากต้องใช้ React ในการแมป custom props ไปยัง props จริงบน DOM element เราจะต้อง rerender ทุกครั้งที่มีค่าแอนิเมชันใหม่ ซึ่งไม่เหมาะสม อย่างไรก็ตาม นี่คือสิ่งที่เกิดขึ้นพอดีเมื่อเราไม่เปิดให้คอมโพเนนต์ custom ของเรารองรับ ref ดังนั้น เวอร์ชัน 5 จะทำงานได้แม้ว่าเราจะยังคงใช้ props แบบกำหนดเอง backgroundColor แทน style อย่างไรก็ตาม เวอร์ชัน 6 จะไม่ทำงาน และคุณสมบัติ backgroundColor จะถูกเพิ่มเข้าไปในองค์ประกอบ DOM ที่มีการตั้งค่า ref ซึ่งจะไม่ทำให้เกิดการเปลี่ยนแปลงใด ๆ เนื่องจากคุณสมบัตินี้ไม่ใช่คุณสมบัติบนองค์ประกอบ DOM div พื้นฐาน

กล่องทราย

ฉันได้จัดทำกล่องทรายสองกล่องให้ใช้งานแล้ว:

  • แซนด์บ็อกซ์แรกแสดงส่วนประกอบในคำตอบนี้ แต่ละส่วนประกอบจะพิมพ์บนคอนโซลทุกครั้งที่มีการเรนเดอร์ ตรวจสอบการพิมพ์เหล่านี้และยืนยันพฤติกรรมที่อธิบายไว้ด้านบน แซนด์บ็อกซ์

  • แซนด์บ็อกซ์ที่สองมีธีมเดียวกันแต่ซับซ้อนขึ้นเล็กน้อย ที่นี่จำนวนครั้งในการเรนเดอร์ถูกอัปเดตอย่างต่อเนื่อง เพื่อให้เราสามารถตรวจสอบพฤติกรรมที่แตกต่างกัน สิ่งหนึ่งที่ได้เรียนรู้จากแซนด์บ็อกซ์นี้คือ การเปลี่ยนแปลงที่มีอนิเมชันทั้งหมดที่เราทำโดยใช้ API เป็น "ฟรี" ในแง่ที่ว่าไม่เพิ่มจำนวนครั้งในการเรนเดอร์ของคอมโพเนนต์ที่เกี่ยวข้อง เมื่อพาเรนต์ทำการเรนเดอร์ซ้ำ ตามปกติ ลูกทั้งหมดจะเรนเดอร์ซ้ำเช่นกัน รายการของประเด็นสำคัญได้ถูกเพิ่มไว้ด้านล่าง แซนด์บ็อกซ์

ข้อสรุป

  • ควรใช้วิธีการของ API เสมอเมื่อใช้ hook ของ react-spring
  • ทำให้ส่วนประกอบของคุณรับค่า ref ได้เสมอเมื่อห่อมันด้วย animated.
  • หากคุณต้องการให้สามารถอัปเดตคอมโพเนนต์ที่ถูกห่อหุ้มโดยไม่ต้องเรนเดอร์ใหม่ คุณสามารถแนบพฤติกรรมของ react-spring กับองค์ประกอบได้เพียงตัวเดียว react-spring ทำการอัปเดตโดยใช้ ref และเนื่องจากคุณไม่สามารถแนบ ref กับหลายองค์ประกอบพร้อมกันได้ (และ forwardRef ก็ไม่ได้ให้วิธีการเพิ่มหลาย ref) คุณจึงไม่สามารถใช้สปริงของคุณในหลายที่ภายในคอมโพเนนต์ที่ถูกห่อหุ้มโดยไม่ต้องเรนเดอร์ใหม่ มิฉะนั้นคุณจะไม่ได้ฟังก์ชันที่คาดหวัง สำหรับคอมโพเนนต์เช่นนี้ จะดีกว่าที่จะส่ง SpringValue<T> เป็น props และใช้ animated native elements ภายในคอมโพเนนต์แทน

อย่า:

const NestedComp = ({ style1, style2 }) => {
    ...
    return (
        <div style={ style1 }>
            <div style={ style2 }>
                ....
            </div>
        </div>
    )
}

const Wrapped = animated(NestedComp)

ทำ :

const NestedComp = ({ springStyle1, springStyle2 }) => {
    ...
    return (
        <animated.div style={ springStyle1 }>
            <animated.div style={ springStyle2 }>
                ....
            </animated.div>
        </animated.div>
    )
}
  • ตรวจสอบให้แน่ใจว่าคุณสมบัติแบบกำหนดเองที่คุณใช้ในอนิเมชันบนองค์ประกอบของคุณเข้ากันได้กับคุณสมบัติบนองค์ประกอบ DOM พื้นเมืองที่คุณแนบ ref มิฉะนั้น react-spring จะไม่สามารถอัปเดตองค์ประกอบโดยตรงได้อย่างถูกต้อง (แทนที่จะทำเพียงตั้งค่าคุณสมบัติซึ่งถ้ามันไม่มีอยู่บนองค์ประกอบ DOM จะไม่มีผลใด ๆ)

อย่า:

const Comp = ({ color }) => {
    ...
    return (
        <div style={{ backgroundColor: props.color }} />
    )
}

const WrappedComp = animated(Comp)

const Parent = () => {
    ...
    const [springProp, api] ) = useSpring(() => ({ color: "green" }))
    ...
    return <WrappedComp color={ springProp.color } />
}

ทำ :

const Comp = ({ style }) => {
    ...
    return (
        <div style={props.style} />
    )
}

const WrappedComp = animated(Comp)

const Parent = () => {
    ...
    const [springProp, api] ) = useSpring(() => ({ color: "green" }))
    ...
    return (
         <WrappedComp 
             style={{ backgroundColor: springProp.color }}
         />
    )
}

ในตัวอย่างข้างต้น เราต้องการอัปเดต Comp โดยไม่ใช้ React แต่ React จำเป็นในการประกอบ style prop ให้ถูกต้องเนื่องจาก color ไม่ใช่ prop พื้นฐานขององค์ประกอบ div ดังนั้นการอัปเดต color ผ่าน ref จะทำให้ได้เพียงการตั้งค่า color prop บน div ซึ่งจะไม่เกิดผลใด ๆ ในทางกลับกัน เมื่อเราเพิ่มคุณสมบัติ style ที่มีอยู่แล้วในพาเรนต์ องค์ประกอบ animated จะตรวจพบว่า หนึ่งใน style props เป็น SpringValue และการอัปเดตมันตามนั้นจะได้ผลลัพธ์ตามที่คาดไว้

สุดท้าย จำไว้ว่าถ้าเราไม่ได้ต้องการอัปเดตคอมโพเนนต์ที่กำหนดเองของเราด้วย API เราก็สามารถละเว้นจากการออกแบบคอมโพเนนต์ที่กำหนดเองของเราให้สามารถรับ ref และใช้ชื่อ prop ไหนก็ได้ที่เราต้องการ; react-spring จะทำการเรนเดอร์คอมโพเนนต์ใหม่ในทุกเฟรมของแอนิเมชันอยู่แล้ว ดังนั้น React จะทำการแมป prop ของคัสตอมทั้งหมดไปยัง prop ขององค์ประกอบ DOM ที่ถูกต้อง อย่างไรก็ตาม กลยุทธ์การดำเนินการนี้คงไม่เป็นที่ต้องการในความหวัง

คำตอบ #2

แค่เริ่มการสนทนาเท่านั้น

เริ่มต้นด้วยตัวอย่างจากเอกสาร สมมติว่าคุณมีคอมโพเนนต์ Donut ของบุคคลที่สาม มันมีคุณสมบัติ percent และคุณต้องการทำแอนิเมชันตามคุณสมบัตินี้ ดังนั้นคุณสามารถใช้ animated เป็นตัวห่อรอบ ๆ Donut ได้

const AnimatedDonut = animated(Donut)
// ...
const props = useSpring({ value: 100, from: { value: 0 } })
return <AnimatedDonut percent={props.value} />

มันคือที่ที่เกิดปัญหาหรือไม่?

บทความที่เกี่ยวข้อง