Using Mappers to Organize Your Data

Improve the organization of your components

 

At some point during the development of your project, you might have needed to develop a feature that consumed data from an external source. You had no idea, however, about the shape of the data. All you had is a wireframe or a mock of the screen.

Undoubtedly, you’ve called your backend co-worker to ask for a sample of the API response so you can identify the elements you need. But you haven’t gotten it right away, so you turned your brain into the “I need this, how can I guess it?” mode.

In such a situation, you need to be able to fake out an API response using the wireframe as a reference. But what happens when you finally obtain the real API response from your backend co-worker? There would be a lot of places to replace the fake API response, including components and tests. How can you minimize the number of changes and benefit the application code as a side-effect?

The Problem

It’s important to make your components independent of the API response format. — Talysson de Oliveira

Disclaimer: It might seem the Mapper is a design pattern, but it isn’t. A design pattern could probably be used to solve the same problem but not as easily as the Mapper does.

In the test code below, you can see the issue where we couple our components to the API response consumed by them:

// Not the best way to write components and tests, having different names for same data
const UserProfile = ({ name, lastName, birthDate, userEvaluation }) => {
  const fullName = `${name} ${lastName}`
  ...
}

describe('UserProfile', () => {
  it('renders with correct props', () => {
    const MOCK_USER = {
      name: 'Abner', 
      lastName: 'Alves', 
      birthDate: '31/02/3019', 
      userEvaluation: 'topster'
    };

    const wrapper = shallow();
    ...
  })
})

// But if the API response doesn't have the same shape of our mock, our component will stop working

describe('UserProfile', () => {
  it('renders with correct props', () => {
    const ACTUAL_API_RESPONSE = {
      userName: "Talysson",
      userLastName: "Cassiano",
      birth: "01/04/1992",
      evaluation: {
        average: 4.5,
        total: 20,
      }
    }

    const wrapper = shallow();
    ...
  })
})

Instead of that, you can use a mapper to protect your code from changes to the data shape and allow your team to respond faster to changing requirements.

The Solution: Mapper

The mapper is really simple. It consists in a schema with the structure of the data in the form that you prefer, and the moment you receive a response from the API, the data will be tailored to your component’s input format. All the mapper does is map input to output:

Mapping ExampleMapping Example

Creating mappers can come in handy when you have to deal with data from external sources but you have no control over the data. Good use cases include Unit Testing and Components (not only React Components).

Using Mapper on Components

Imagine a scenario where you have several components developed by different people from your team. Each one decides to name the props as they wish, and you end up with a few components having props named equal to the API response.

Without the application of the mapper technique, any change to the source and structure of the data returned by the API would have greatly increased its complexity. You would need to review various components and unit tests. Therefore, a mapper also helps standardize the input format required by your components.

Here are two examples of how this technique would improve your code without the need to perform a lot of data manipulation. Imagine that the following userMapper variable is the result of applying the API response to a mapper function (we’ll show the mapper function in the next section):

// Without Mapper
const UserName = ({name}) => (
   <p>{name}</p>
)

const UserBio = ({firstName, lastName, birthDate}) => (  //...
  <p>{firstName} {lastName} - {birthDate}</p>
)

// Mapper receives an object and return the formatted object
const userMapper = (data) => {
  firstName: data.name,
  lastName: data.lastName,
  fullName: `${data.name} ${data.lastName}`
  birth: data.birthDate,
};

// Provide api response to the mapper
const userData = mapper(apiResponse);

// You will the userMapper data in your components
const UserName = ({firstName}) => (
  <p>{firstName}</p>
}
// <UserName {...userData} />

const UserBio = ({fullName, birth}) => (
  <p>{fullName} - {birth}</p>
)

// <UserBio {...userData} />

As you can see, the mapper acts as a filter on the API response, which can then be applied to both User components uniformly.

Using Mapper on Unit Tests

In the next example, you have not only unit tests, but a complete example of how the mapper should work. Besides being used within your code, the mapper can also be used by your unit tests.

In the following test sample, we are handing a stubbed API_RESPONSE to apiResponseMapper — which is our mapper function — before passing the result on to the UserProfile and UserRating components.

// Your API Response or another data source
const API_RESPONSE = {
  userName: "Talysson",
  userLastName: "Cassiano",
  birth: "01/04/1992",
  evaluation: {
    average: 4.5,
    total: 20,
  }
}

const apiResponseMapper => data => {
  const {
    userName,
    userLastName
    birth,
    evaluation
  } = data

  return {
    name: userName,
    lastName: userLastName,
    fullName: `${userName} ${userLastName}`,
    birthDate: new Date(birth),
    userRating: evaluation.average,
    ratingsTotal: evaluation.total
  }
}

const userDataMapper = ({ data, origin }) => {
  const mappers = {
    API: apiResponseMapper
  }

  return mappers[origin] ? mappers[origin](data) : data
}

// Good way to write components and tests, having same names in both components for the same data
const UserProfile = ({ fullName, birthDate, userRating }) => {...}

describe('UserProfile', () => {
  it('renders with correct props', () => {
    const MOCK_USER = userDataMapper({ data: API_RESPONSE, origin: 'API' })
    ...
  })
})

const UserRating = ({ fullName, userRating, ratingsTotal }) => {... }

describe('UserRating', () => {
  it('renders with correct props', () => {
    const MOCK_USER = userDataMapper({ data: API_RESPONSE, origin: 'API' })
    ...
  })
})

Note that the Mapper entry point, userDataMapper, takes two parameters: data and origin.

  • `data` is the API response;
  • `origin` allows us to associate mappers with different data sources. In our example, we are using the `“API”` data source, which is handled by the `apiResponseMapper` function. Of course, we can specify mappers for other data sources on an as-needed basis.

Conclusion

The mapper is not a silver bullet, but you can certainly take advantage of it — whether in small projects or in projects where you have many Data Sources or a large team.

This technique will certainly help to keep your code clean, well organized and drier, thus avoiding a mess of names, props, mocks, issues, and bugs caused due to changes in the third party API.

True Story: I would have really liked to have this concept in my head months ago before I started to work on my new project. That would have saved many mistakes and the migration to a new API.

Thanks to Talysson de Oliveira, Luan Gonçalves, and Willame Soares.