Animate Your Web Sections Dynamically with useSection Hook

Animate Your Web Sections Dynamically with useSection Hook

Sometimes, you may want to trigger an animation for a specific section based on whether the user has switched to that section. Here’s how you can achieve this:

Usage

By using this hook, you can run animations based on scroll direction while maintaining consistency between transitions. In certain scenarios, you may want to trigger different animations depending on the section you’re navigating from or to or even the direction if going forward or backward. We'll be building this hook in this post.

// SecondSection.tsx
import { Section } from './enums.ts'

export default function SecondSection() {
  const { active } = useSection(
    () => ({
      section: Section.Second,
      enter(back) {
        // fade-in
        if(!back) ...  // when coming from previous (1 -> 2)
        else ...       // when coming from next (3 -> 2)
      },
      leave(back) {
        // fade-out
        if(!back) ...  // when going forward (2 -> 3)
        else ...       // when going backward (2 -> 1)
      },
    }),
    [],
  );

  return ...
}
Enter fullscreen mode Exit fullscreen mode

Configuration

1. Define Sections as an Enum

First, create an enum to represent your sections. For example:

// enums.ts
enum Section {
  First,
  Second,
  Third,
}
Enter fullscreen mode Exit fullscreen mode

2. Global State Management

To manage global state, consider using a library like zustand. Here’s an example of setting up a store:

// store.ts
import { create } from 'zustand'

const useStore = create((set) => ({
  section: Section.First,
  setSection: (section) => set((state) => ({ section })),
}))
Enter fullscreen mode Exit fullscreen mode

3. Watch Section Changes

Create a custom hook called usePrevious that stores the previous value and triggers an effect when the section changes:

// usePrevious.ts
import { useEffect, useRef } from "react";

export default function usePrevious<T extends any[]>(fn: (prev: T) => any, deps: T) {
  const previousDepsRef = useRef<T>(deps);

  useEffect(() => {
    const eject = fn(previousDepsRef.current);
    previousDepsRef.current = deps;
    return eject;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
}
Enter fullscreen mode Exit fullscreen mode

4. Create the useSection Hook:

Finally, implement the useSection hook with four events: enter, leave, enterBack, and leaveBack. This hook will help you manage section transitions:

// useSection.ts
import { DependencyList, useMemo, useState } from "react";
import usePrevious from "@/hooks/usePrevious";
import { useStore } from "@/store";
import { Section } from "@/ts/enums";

type Config = {
  section: Section;
  enter?: (from: Section, back: boolean) => Function | void;
  leave?: (to: Section, back: boolean) => Function | void;
};

export default function useSection(
  getConfig: () => Config,
  deps: DependencyList,
) {
  const section = useStore((s) => s.section);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  const config = useMemo(getConfig, deps);
  const [target] = useState<Section>(config.section);

  // same as useEffect but with previous values
  usePrevious<[Section, Config | undefined]>(
    ([fromSection]) => {
      if (section === target) {
        const back = target < fromSection;
        return config.enter?.(fromSection, back);
      }
      if (fromSection === target) {
        const back = target > section;
        return config.leave?.(section, back);
      }
    },
    [section, config],
  );

  const active = useMemo(() => section === target, [section, target]);

  return { section, active };
}
Enter fullscreen mode Exit fullscreen mode

Feel free to explore the implementation on my portfolio website. Let me know if you have any questions!