Dynamically Rendering React Components

Posted by Niky Morgan on January 26, 2019

My most recent project is an application I’ve dubbed ‘Recommendationer’. (The name is as much a work-in-progress as the app itself.) The app allows friends to save content to entertain each other. They can submit photo links, YouTube videos, search and embed music from Spotify as well as recommend books, TV and movies. With so many types of data, it can be a struggle to keep an app DRY. Fortunately if we treat our content all the same on the Rails backend, we can rely on our React frontend to dynamically render the matching component for the content type.

Already the app has to display six different types of content, and this could well expand. We should set up the app to be able to view each type of content individually, but also be able to view all content at once. The simplest way to do that is to create one Content resource in our backend that all these different things can live under. When we query our server at /contents, we retrieve all the content saved. However, we can query content individually by sending over a parameter, e.g. /contents?type=music will return only content of type music.

Our content has a recommender who shared it and associated links. The last piece of data we need to include on our Contents table is an enum indicating what type of content it is. If you are curious about enums, read my earlier post: Saving Database Space with Rails Enums. This enum column is what allows us to find and return only music when a request is made to our server at /contents?type=music. Our controller uses the type parameter from the URL and searches for rows in the database where the type matches the parameter.

With this setup, our backend knows that it can treat all content the same, but it also knows how to differentiate between types. Now to setup our frontend. At this point we’ve been able to maintain some semblance of DRYness by treating all content the same, but that goes out the window in our frontend. Embedding YouTube and Spotify content requires iFrames. Photos require image tags. Each type of content requires a different presentation in our frontend, meaning a different component.

When we request content data from our backend, we’ll receive an array of content back. A common React pattern is to have a container or list component that iterates through a list of items and returns a component for each. In the example below, let’s say the ContentContainer is receiving an array of music as props which we destructure and assign to a variable in the function signature. Then we map through the array and return an array of MusicComponents which have been given all of the attributes of the music object as props. Our ContentContainer then renders this array of components.

const ContentContainer = ({ content }) => {
  const items = content.map(el => (
    <li key={el.id}>
      <MusicComponent {...el} />
    </li>
    )
  )

  return (
    <ul>
      { items }
    </ul>
  )
}

This pattern works well for a scenario when we are putting only one type of content on the page. However, in reality we have to tell our ContentContainer to use music components if the content is music, book components if the content is books, etc. If the content is mixed, it has to look at each item and determine what it is before deciding which component should render it. A switch statement could solve this problem for us by matching the type property on the object to a matching component and returning it.

We could create a function that takes an object, passes it through the switch statement and returns a component for that element. This function, our ContentComponentSelector, only returns one component (or null) at a time. However, when we use it in the callback argument of our map function, we can create an array of many different types of components.

const ContentComponentSelector = (el) => {
  // inspect el, determine type of content
  // based on type attribute and return correct component
  switch(el.type) {
  case 'music':
    return <MusicComponent {...el} />
  case 'books':
    return <BookComponent {...el} />
  // and so on...
  default:
    return null
  }
}

const ContentContainer = ({ content }) => {
  const items = content.map(el => (
    <li key={el.id}>
      <ContentComponentSelector {...el} />
    </li>
    )
  )

  return (
    <ul>
      { items }
    </ul>
  )
}

While this switch statement works, we can go one step further and create a dynamic solution without conditional logic. Using a components object, ContentComponentSelector can call components[el.type] and get access to the variable holding a reference to the related component.

const components = {
  book: Book,
  movie: Movie,
  tv: TV,
  music: Music,
  youtube: YouTube,
  photo: Photo,
}

const ContentComponentSelector = (el) => {
  // el contains all of the properties of our content object
  const MatchingComponent = components[el.type]
  // MatchingComponent now points to one of the components
  // stored as a value in the components object
  return (
    <li className='content margin-s'>
      <MatchingComponent {...el} />
    </li>
  )
}

const ContentContainer = ({ content }) => {
  const items = content.map(el => (
    <li key={el.id}>
      <ContentComponentSelector {...el} />
    </li>
    )
  )

  return (
    <ul>
      { items }
    </ul>
  )
}

Now we are using our ContentComponentSelector to intelligently render the right component based on the props it receives. This means our ContentContainer works no matter what type of content we use. Additionally, instead of using an imperative switch statement, we were able to make our code slightly more declarative by using an object.

Recommendationer