Repeat component in React like *ngFor in Angular

Viewed 230

In React, we use map function inside JSX to iterate on items.

Is it possible to have a Repeat component like *ngFor in Angular?

<Repeat for={[1, 2, 3]}>
    ...
    <Repeat for={[5, 6, 7]}>
        ...
    </Repeat>
</Repeat>

If yes, Is it possible to have some contextual properties inside each Repeat component?

  • $item : the item in the array.
  • $index : the index of the item in the collection.
  • $first : is true if the item is the first item in the array.
  • $last : is true if the item is the last item in the array.

Is it possible to write such a component in React?

2 Answers

So there are two approaches to write this:

  1. Using React.cloneElement() to clone children with additional props.
  2. Using render props to call children prop as a function.

React.clone()

You can use React.Children to iterate over the children, and then clone each element with new props (shallow merged) using React.cloneElement.

const Repeat = ({ for: items = [], children }) => {
  return items.map((item, index) => {
    const childrenWithProps = React.Children.map(children, (child) => {
      // Checking isValidElement is the safe way and avoids a typescript
      // error too.
      if (React.isValidElement(child)) {
        return React.cloneElement(child, {
          item,
          index,
          first: index === 0,
          last: index === items.length - 1
        });
      }
      return child;
    });

    return <div>{childrenWithProps}</div>;
  });
};

const Span = ({ item, index, children }) => (
  <span>
    {children}, {item}
  </span>
);


function App() {
  return (
    <div className="App">
      <h1>React Clone</h1>
      <Repeat for={[1, 2, 3]}>
        <Span>Outer</Span>
        <Repeat for={[4, 5, 6]}>
          <Span>Inner</Span>
        </Repeat>
      </Repeat>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("container"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<div id="container"></div>

Render Props

You can pass props to children with render props. In this approach, the children prop is a function which can accept any arguments you want to pass and returns the children:

const Repeat = ({ for: items = [], children }) => {
  return items.map((item, index) => {
    return children({
      item,
      index,
      first: index === 0,
      last: index === items.length - 1
    });
  });
};

function App() {
  return (
    <div className="App">
      <h1>Render Props</h1>
      <Repeat for={[1, 2, 3]}>
        {({ item, index }) => (
          <React.Fragment>
            <div>Outer, {item}</div>
            <Repeat for={[4, 5, 6]}>
              {({ item, index }) => (
                <React.Fragment>
                  <div>Inner, {item}</div>
                </React.Fragment>
              )}
            </Repeat>
          </React.Fragment>
        )}
      </Repeat>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("container"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<div id="container"></div>


Original Answer:

Yes.

import React from 'react';

export const Repeat = ({ for: items = [], children }) => {
  return items.map((item, index) => {
    return React.cloneElement(children, {
      item,
      index,
      first: index === 0,
      last: index === items.length - 1
    });
  });
};

I use React.cloneElement to pass props to children. There are other techniques as well if you don't like this one. As this one has a limitation that <Repeat> can have only one child element

Usage:


import { Repeat } from './Repeat';

function Child({ item, index, first, last }) {
  return (
    <div>
      <span>Item: {item}</span>
      <span>, Index: {index}</span>
      <span>, First: {String(first)}</span>
      <span>, Last: {String(last)}</span>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <Repeat for={[1, 2, 3]}>
        <Child />
      </Repeat>
    </div>
  );
}

Related