DEV Community

Cover image for Advance React Patterns: Render Props
Sanjeev Sharma
Sanjeev Sharma

Posted on • Edited on

Advance React Patterns: Render Props

Hi 👋

If you have been working with React for quite some time, you must be well versed in writing reusable components. Reusable UI components! But as the codebase scales you often end up in situations where you want to share the business logic but UI may be different.

💡 Situations like these are a perfect opportunity to use some advanced patterns. Render Props is one such pattern.

🚀 Some libraries that use render props pattern include React Router, Downshift and Formik.

Let's start with an example.

For instance, you are building an online store that showcases products.

There is some common business logic that is required by every product list:

  1. ✈️ Navigate to a page product page when clicked
  2. 🤑 Sorting products based on price
  3. 💾 Saving products
    • save in local storage if user is not logged in
    • otherwise store it in the DB using an API call

🎨 But based on where the list is being rendered UI can be different too. At one place you want to show stats, or product image, at other place you just want to show the title.

🧠 Let's understand the basic anatomy of a render props component, first.


const Wrapper = ({ products, render }) => {

  // do some stuff
  const updatedProducts = someOperations(products)

  // provide some helper funcs for data like sort func
  const sort = () => {}

  return render({ updatedProducts, sort })
}


Enter fullscreen mode Exit fullscreen mode

👉 Render Props components are just wrappers around your UI components. The component above gets two props - products and render. products is the data that needs to be modified using business logic and render is a function where this modified data and some other helper functions are passed.

🤔 But how do I use this component?

// import everything

const HomeScreenProducts = () => {

  // assume you have this hook
  const { products } = useData()

  return (
    <ProductsWrapper 
     products={products}
     render={
       ({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />)
     }
    />
  )

}

Enter fullscreen mode Exit fullscreen mode

👉 Our HomeScreenProducts component uses ProductsWrapper and lets it handle all the business logic. UI is still controlled by the calling component.
Inside render function we consume the modified products data and render UI based on it.

😰 This looks a bit complicated. But we can simplify it to have a much cleaner API. Instead of passing a render function separately we can use the children prop.

After updating both the components look like this.


const Wrapper = ({ products, children }) => {

  // same stuff here

  return children({ updatedProducts, sort })
}

Enter fullscreen mode Exit fullscreen mode

const HomeScreenProducts = () => {

  // same stuff here

  return (
    <ProductsWrapper products={products}>
      {({ updatedProducts, sort }) => updatedProducts.map(product => <ProductCard />}
    </ProductsWrapper>
  )

}

Enter fullscreen mode Exit fullscreen mode

👌 This is much better. The children prop is serving the same purpose as the render prop we had earlier. This way of writing render props is more common.

⚠️ Don't forget to add key to your list.

💪 Now that we understand render props pattern, we can complete our task mentioned earlier.

import React from "react";
import { useAuth } from "./hooks/useAuth";

const ProductsWrapper = ({ products, children }) => {
  const { isLoggedIn } = useAuth();
  const [sortedProducts, setSortedProducts] = React.useState(products);

  const sort = (order) => {
    const reorderedProducts = [...products];

    reorderedProducts.sort((a, b) => {
      if (order === "desc") {
        return b.price > a.price;
      } else {
        return a.price > b.price;
      }
    });

    setSortedProducts(reorderedProducts);
  };

  const save = (productId) => {
    if (isLoggedIn) {
      // call API
      console.log("Saving in DB... ", productId);
    } else {
      // save to local storage
      console.log("Saving in local storage... ", productId);
    }
  };

  const navigate = () => {
    console.log("Navigating...");
  };

  return children({ sortedProducts, sort, save, navigate });
};

export default ProductsWrapper;

Enter fullscreen mode Exit fullscreen mode

We extend our ProductsWrapper component and add all the required functionality to it. It calls children as function and passes the data and helpers functions.

import ProductsWrapper from "./ProductsWrapper";

const products = [
  { id: 1, name: "Coffee", price: 2 },
  { id: 2, name: "Choclates", price: 3 },
  { id: 3, name: "Milk", price: 5 },
  { id: 4, name: "Eggs", price: 4 },
  { id: 5, name: "Bread", price: 1 }
];

export default function App() {
  return (
    <div className="App">
      <ProductsWrapper products={products}>
        {({ sortedProducts, sort, save, navigate }) => (
          <>
            <div className="flex">
              <button onClick={() => sort("desc")}>Price: High to Low</button>
              <button onClick={() => sort("asc")}>Price: Low to High</button>
            </div>

            {sortedProducts.map((product) => (
              <div className="product" key={product.id}>
                <span onClick={() => navigate(product.id)}>
                  {product.name} - ${product.price}
                </span>
                <button className="save-btn" onClick={() => save(product.id)}>
                  save
                </button>
              </div>
            ))}
          </>
        )}
      </ProductsWrapper>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Our UI component uses ProductsWrapper and takes care of the UI. As you can see, we are free to modify the UI or create other UI components that look totally different from this one. Our business logic resides at one place.

If you want to play around with the example, it's available on codesandbox: https://codesandbox.io/s/render-props-example-6190fb

That's all folks! 👋

🤙 If this helped you, consider sharing it and also connecting with me on LinkedIn and Twitter.

Top comments (1)

Collapse
 
zyabxwcd profile image
Akash

Woah!! Cooool