React / Render Props Pattern

Render Props Pattern

"Render Props" бол component-ын логикийг дахин ашиглах арга — component нь "юу render хийх"-ийг шийддэггүй, харин ашиглагч нь props-оор функц дамжуулж render-ийг хянадаг. Нэр нь хүндрэлтэй сонсогдох ч ойлгомжтой жишээ харвал тэр дор ойлгоно. Hooks гарч ирснээс хойш энэ pattern харьцангуй хоёрдогч болсон ч том сан, бодит проектуудад өнөөг хүртэл элбэг тааралддаг тул ойлгох нь чухал.

Render Props гэж юу вэ?

Нэг жишээгээр харвал:

tsx
// Render Props ашигласан component
// "children" нь функц байна — энэ л render props-н үндсэн санаа
<MouseTracker>
  {({ x, y }) => (
    <div>
      Хулганы байрлал: {x}, {y}
    </div>
  )}
</MouseTracker>

MouseTracker нь хулганы байрлалыг хянадаг. Гэхдээ тэр байрлалыг хэрхэн харуулах нь MouseTracker-н хариуцлага биш — хэрэглэгч нь функц дамжуулж өөрөө шийднэ. Иймд нэг MouseTracker component-ыг ямар ч UI-тэй хослуулж болно.

Үндсэн жишээ: MouseTracker

tsx
// src/components/MouseTracker.tsx
import { useState, useEffect, ReactNode } from "react";

interface MousePosition {
  x: number;
  y: number;
}

interface MouseTrackerProps {
  children: (position: MousePosition) => ReactNode;
}

function MouseTracker({ children }: MouseTrackerProps) {
  const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener("mousemove", handleMove);
    return () => window.removeEventListener("mousemove", handleMove);
  }, []);

  // children нь функц — байрлалыг дамжуулж дуудна
  return <>{children(position)}</>;
}

export default MouseTracker;

Ашиглах жишээ — нэг MouseTracker, олон харагдах байдал:

tsx
// Координат харуулах
<MouseTracker>
  {({ x, y }) => <p>X: {x}, Y: {y}</p>}
</MouseTracker>

// Дагаж явдаг элемент
<MouseTracker>
  {({ x, y }) => (
    <div
      style={{
        position: "fixed",
        left: x - 12,
        top: y - 12,
        width: 24,
        height: 24,
        borderRadius: "50%",
        background: "#60a5fa",
        pointerEvents: "none",
      }}
    />
  )}
</MouseTracker>

// Тодорхой хэсэгт орвол мессеж харуулах
<MouseTracker>
  {({ y }) => (
    y > 300
      ? <div className="banner">Дээш гүй!</div>
      : null
  )}
</MouseTracker>

render prop ашиглах хувилбар

children props-оор функц дамжуулахын оронд нэрлэсэн prop ашиглаж болно:

tsx
// src/components/DataFetcher.tsx
import { useState, useEffect, ReactNode } from "react";

interface FetchState<T> {
  data: T | null;
  isLoading: boolean;
  error: string | null;
}

interface DataFetcherProps<T> {
  url: string;
  render: (state: FetchState<T>) => ReactNode; // нэрлэсэн render prop
  loadingFallback?: ReactNode;
}

function DataFetcher<T>({ url, render, loadingFallback }: DataFetcherProps<T>) {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    isLoading: true,
    error: null,
  });

  useEffect(() => {
    setState({ data: null, isLoading: true, error: null });

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`Алдаа: ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((data) => setState({ data, isLoading: false, error: null }))
      .catch((err: Error) =>
        setState({ data: null, isLoading: false, error: err.message }),
      );
  }, [url]);

  if (state.isLoading && loadingFallback) return <>{loadingFallback}</>;

  return <>{render(state)}</>;
}

export default DataFetcher;

Ашиглах жишээ:

tsx
interface Course {
  slug: string;
  title: string;
  lessonCount: number;
}

function CoursePage() {
  return (
    <DataFetcher<Course>
      url="/api/courses/javascript"
      loadingFallback={<p>Ачааллаж байна...</p>}
      render={({ data, error }) => {
        if (error) return <p className="error">{error}</p>;
        if (!data) return null;

        return (
          <div>
            <h1>{data.title}</h1>
            <p>{data.lessonCount} хичээл</p>
          </div>
        );
      }}
    />
  );
}

Render Props vs Custom Hook

Render Props болон custom hook хоёулаа логик дахин ашиглахад хэрэглэгддэг. Тэдгээрийн ялгааг ойлгох нь чухал:

tsx
// Render Props — логик + render хамтдаа
function WithMousePosition({
  children,
}: {
  children: (pos: MousePosition) => ReactNode;
}) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  // ... event listener логик ...
  return <>{children(pos)}</>;
}

// Custom Hook — зөвхөн логик
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  // ... event listener логик ...
  return pos;
}

// Hook-тэй хэрэглэх — илүү энгийн
function MyComponent() {
  const { x, y } = useMousePosition();
  return (
    <p>
      X: {x}, Y: {y}
    </p>
  );
}

Орчин үеийн React-д custom hook нь ихэнх тохиолдолд Render Props-оос илүү тохиромжтой. Гэхдээ Render Props нь дараах тохиолдолд давуу байдаг:

  • Логикийг component tree-д нэгтгэх шаардлагатай үед
  • Хуучин class component-тэй ажиллахад (hook ашиглаж болохгүй)
  • Гуравдагч сангуудын API-д render хяналт шаардлагатай үед

Render Props Pattern-ийг практикт харвал танихгүй байхгүй — react-router<Route render={...}>, react-table-н render функцүүд зэрэг олон алдартай сан энэ аргыг ашигладаг. Ойлгоод авсан ухаан чинь хаана ч ажилд хэрэглэгдэнэ.

Дараагийн хичээлд:

Higher-Order Component (HOC) Pattern — component-ыг "боож" нэмэлт чадвар олгох арга. React-н хамгийн хуучин боловч өнөөг хүртэл өргөн хэрэглэгддэг pattern-уудын нэгийг судална.