Search code examples
javascriptreactjsrenderingreact-props

React rendering logic as component prop vs callback function


Say a component needs to render a bunch of elements, but the rendering has to be customizable, because this component would be re-used in many places. Think of it like a calendar component that needs to render events, but the event rendering logic should be controllable through the calendar component props.

So far I found 2 ways of doing this and I'm not sure what is the right and wrong way, if there are drawbacks of one compared to another

1- passing a callback that does the rendering

type MainProps = {
   renderStuff: (stuff) => void;
};

function MainComponent(props: MainProps){
   const matchingStuff = [1, 2, 3...]

   return (
     <ul>
       {matchingStuff.map(stuff => <li>{renderStuff(stuff)}</li>)}
     </ul>
   );
}

2- passing a component definition as a prop:

type StuffProps = {
  stuff: number;
}

type MainProps = {
  renderStuff: FC<StuffProps>;
};

function MainComponent(props: MainProps){
  const matchingStuff = [1, 2, 3...]

  return (
    <ul>
      {matchingStuff.map(stuff => <props.renderStuff stuff={stuff} />)}
    </ul>
  );
}

function Stuff(props: StuffProps){
  return <li>{props.stuff}</li>
}

(notice the type difference for renderStuff)


Solution

  • I'm not sure what is the right and wrong way

    Nope, no right or wrong way, both work (though not in the same way - see below) and are patterns present in various, high quality react libraries.

    Some react libraries actually used both at the same time: https://v5.reactrouter.com/web/api/Route

    It's just a means of presenting an API.

    In certain circumstances you'll find you can't use one, or that one is more performant than the other. But you'll generally know those circumstances when you see them.

    The biggest gotcha will come if you pass an inline function and render it as a component, like this:

    const MyParent = () => <MyChild Component={({prop}) => <>Some Stuff - {prop}</>}/>
    
    const MyChild = ({Component}) => <Component prop={"Render me!"}/>
    

    In the example above I posted, it's inconsequential because there's no state in the inline function. If there was state, you'd see that every time MyParent gets rendered, the state would get reset in MyChild because it's a new component (inline function) every single time.

    In the above example, you'd declare the component outside the render function and pass it as a prop like this:

    const MyParentRenderComponent = ({prop}) => <>Some Stuff - {prop}</>
    
    const MyParent = () => <MyChild Component={MyParentRenderComponent}/>
    

    This is a standard rule in react: don't declare components inside the render function (or body of a functional component) so it's not really unique to this case.