Render Props for Code Reusability

Loading

The render props pattern is a powerful technique for sharing code between React components by using a prop that accepts a function which returns JSX. This pattern provides excellent flexibility and composability.

Core Concept

A component with a render prop takes a function that returns React elements and calls it instead of implementing its own render logic.

Basic Example

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

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

  return (
    <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
      {render(position)}
    </div>
  );
}

// Usage
function App() {
  return (
    <MouseTracker
      render={({ x, y }) => (
        <h1>
          The mouse position is ({x}, {y})
        </h1>
      )}
    />
  );
}

Variations of the Pattern

1. Traditional Render Prop

<DataProvider render={data => (
  <ChildComponent data={data} />
)} />

2. Children as a Function

<DataProvider>
  {data => (
    <ChildComponent data={data} />
  )}
</DataProvider>

3. Prop Name Variations

You can use any prop name, not just render:

<MousePosition>
  {(position) => <DisplayPosition {...position} />}
</MousePosition>

<FetchData>
  {({ loading, error, data }) => (
    <Results loading={loading} error={error} data={data} />
  )}
</FetchData>

Key Benefits

  1. Code Reusability: Share behavior across multiple components
  2. Flexibility: Consumers control rendering output
  3. Explicit Data Flow: Clear visibility of what props are being passed
  4. Composability: Easily combine multiple render props components

Practical Examples

1. Data Fetching Component

function FetchData({ url, children }) {
  const [state, setState] = useState({
    loading: true,
    error: null,
    data: null
  });

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => setState({ loading: false, data }))
      .catch(error => setState({ loading: false, error }));
  }, [url]);

  return children(state);
}

// Usage
<FetchData url="/api/users">
  {({ loading, error, data }) => {
    if (loading) return <Spinner />;
    if (error) return <Error message={error.message} />;
    return <UserList users={data} />;
  }}
</FetchData>

2. Authentication Wrapper

function Auth({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkAuth().then(user => {
      setUser(user);
      setLoading(false);
    });
  }, []);

  if (loading) return <LoadingScreen />;

  return children({
    user,
    login: (credentials) => login(credentials).then(setUser),
    logout: () => logout().then(() => setUser(null))
  });
}

// Usage
<Auth>
  {({ user, login, logout }) => (
    user ? (
      <Dashboard user={user} onLogout={logout} />
    ) : (
      <LoginForm onLogin={login} />
    )
  )}
</Auth>

Best Practices

  1. Type Safety: Use PropTypes or TypeScript to document the render function signature
  2. Performance: Wrap render functions in useCallback if defined in parent component
  3. Composition: Combine with other patterns like hooks for maximum flexibility
  4. Clean API: Keep the number of arguments passed to the render function reasonable
  5. Error Handling: Provide graceful fallbacks for error states

TypeScript Implementation

interface MousePositionProps {
  children: (position: { x: number; y: number }) => React.ReactNode;
}

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

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

  return (
    <div onMouseMove={handleMouseMove}>
      {children(position)}
    </div>
  );
}

// Usage
<MousePosition>
  {({ x, y }) => <div>Position: {x}, {y}</div>}
</MousePosition>

Comparison with Other Patterns

Render Props vs HOCs

Render PropsHigher-Order Components
More flexible renderingFixed component structure
Explicit data flowImplicit prop injection
Easier to composeCan suffer from wrapper hell
Better TypeScript supportMore complex typing

Render Props vs Hooks

Render PropsCustom Hooks
Works with class componentsFunction components only
Visual composition in JSXLogic composition in code
More explicit data flowMore concise syntax

Common Pitfalls

  1. Performance Issues: Creating new functions in render can cause unnecessary re-renders
  • Solution: Memoize the render function if needed
  1. Over-nesting: Deeply nested render props can hurt readability
  • Solution: Compose components wisely or consider hooks
  1. Abstraction Overuse: Not every component needs to be a render prop component
  • Solution: Use only when you need true rendering flexibility

The render props pattern remains a valuable tool in React, especially when you need to provide maximum rendering flexibility to component consumers while sharing complex behavior.

Leave a Reply

Your email address will not be published. Required fields are marked *