Home > Software engineering >  React functional component to accept rendering function
React functional component to accept rendering function

Time:09-29

I would like to find out how to write a functional component that would accept an array of objects as prop (dataSource), then render all the array objects based on a RenderFunction which I would pass as another prop. I want the render function to accept a parameter that would represent an object from the array.

Check this code snippet and the comments:

// This is my data

var dummyData = [{
    "id": 1,
    "name": "item1"
  },
  {
    "id": 2,
    "name": "item2"
  },
  {
    "id": 3,
    "name": "item3"
  },
  {
    "id": 4,
    "name": "item4"
  },
  {
    "id": 5,
    "name": "item5"
  },
  {
    "id": 6,
    "name": "item6"
  }
]

// Functional Component : 

function ListComponent(props) {
  return (
    <div className={"ListContainer"}>
      {props.dataSource.map((x) => {
        return (
          <div key={x.id} className="ListItemContainer">
            {props.itemRender}{" "}
          </div>
        );
      })}{" "}
    </div>
  );
}

// This is my render function which I'm passing as a prop.
// I would like it to accept an argument which would become
// an object of dummy data array.
// For example if I passed x to it, the x inside the function on the first 
// .map iteration would become {"id": 1,"name": "item1"}
function itemRenderTest() {
  return ( 
  <p> This is item </p>
    // I want to be able to render it like this
    // <p> This is {x.name} </p>
  )
}

// passing props and rendering
ReactDOM.render(
 <ListComponent
      dataSource={dummyData}
      itemRender={itemRenderTest()}
    />,
  document.getElementById("root")
);
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

The snippet illustrates some of the desired functionality. I can write a render function, but don't know how can I change the code so the render function could accept a parameter which would represent an object from the array.

I want to be able to write the render function like this:

function itemRenderTest(x){
  return(
    <p>This is {x.name}</p>
  )
}

The Component would receive 2 props. The first - dataSrouce would specify the JSON array. The second - render function would define how the child list components are being rendered

<ListComponent
  dataSource={dummyData}
  itemRender={itemRenderTest}
/>

I'm trying to recreate a reusable component similar to what a lot of DevExtreme react components do. They basically just accept a render function definition like this renderItem={renderItemFunction} and it just works. I want to write my List component so it does the same This is a good example of one of their components

Is this possible with React? Code snippets would be really helpful.

CodePudding user response:

That's 100% possible in React and a super common pattern. If I understand your question correctly -- what I typically do in this situation is

  1. Define a parent component for the list items. It will handle fetching or otherwise retrieving the array of objects data, the overall state of that data, and the render logic for the individual list components
  2. ListItem component, which is stateless (pure component) and simply renders reusable components based on data passed in as props. That's how component libraries create reusable components, like the one you mentioned
const ItemsList = () => {
  // state variable which will store the list data
  const [listData, setListData] = useState([])
  
  // let's assume the list items are fetched from an API on initial render
  useEffect(() => {  
    // fetch logic for list items, then update state variable 
    // with resulting data
    const listItems = axios("https://yourapi.com/listitems")
                     .then(({ data }) => setListData(data)
                     .catch(err => console.info("Fetch error", err))
                     )
  }, [])

    const renderList = useMemo(() => listData.map(
     (listItemData) => <ListItem data={listItemData}/>), 
    [listData])

  return (
    <div>
    {renderList}
    </div>
  )
}

const ListItem = (props) => {
  const { data } = props;

  return (
    // populate the component with the values from the data object
  )
}

A few things to point out here:

  • useEffect hook with [] dependency will only run once on first render, retrieve the necessary data, and update the state variable initialized with the useState hook. This can be done in other ways too
  • useMemo hook allows us to define the render logic for the individual list components, and memoize the evaluated result. That way, this function won't run on every render, unless the value of the listData variable changes. The function provided to the useMemo hook iterates through the array of objects, and renders a ListItem components with the respective data
  • The ListItem component then simply receives the data as a prop and renders it

Edit based on the updated answer: I haven't tested it but this approach should work.

const ItemsList = (props) => {
  const { data, itemRender: ItemRender } = props;
  
  const renderItems = () => data.map((itemData) => <ItemRender data={itemData}/>)

  return (
    <div>
    {renderItems()}
    </div>
  )
}

const ListItem = (props) => {
  const { data } = props;

  return (
    // populate the component with the values from the data object
  )
}


const App = () => {
  const data = retrieveData()
  
  return (
    <ItemsList data={data} itemRender={ListItem}/>
  )
}
  • App component retrieves the data, decides on the component that it will use to render the individual item (ListItem) and passes both of those as props to ItemsList
  • ItemsList then simply maps the data to each individual component

Edit 2: basic working snippet

const ItemsList = (props) => {
  const { data, itemRender: ItemRender } = props;
  
  const renderItems = () => data.map((itemData, i) => <ItemRender data={itemData.val} key={i}/>)

  return (
    <div>
    {renderItems()}
    </div>
  )
}

const ListItem = (props) => {
  const { data } = props;
  console.info(data)

  return (
    <div 
     style={{
      width: 100,
      height: 100,
      border: "2px solid green",
      display: "flex",
      alignItems: "center",
      justifyContent: "center"
    }}>
    {data}
    </div>
  )
}


const App = () => {
  const data = [{val: 1}, {val: 2}, {val: 3}]
  
  return (
    <div 
    style={{
      width: "100vw",
      height: "100vh",
      backgroundColor: "white",
      display: "flex",
      alignItems: "center",
      justifyContent: "center"
    }}>
    <ItemsList data={data} itemRender={ListItem}/>
    </div>
  )
}

export default App;
  • Related