rawjs.xyz

26.08.2023

Thinking in Transforms

Transforms, more so know as modifications. When working with data centric apps you'll normally find yourself patching and re-creating the data requirements while modifying the original source.

1const sourceData = fetchUserInformation()
2sourceData.modificationOne = sourceData.something + '| suffix'
3sourceData.modificationTwo = [
4  sourceData.something,
5  sourceData.somethingElse,
6].join(' ')

This code should look familiar if you've worked with scenarios where the received data isn't what you expect it to be and so you write simple modifications around it to get your way with it.

It's not wrong or bad, it just gets hard to track over time.

Example, lets take a scenario where you have the following, you get the user information and right after it you make changes to add in a displayName for it.

1const sourceData = fetchUserInformation()
2sourceData.displayName = [sourceData.firstName, sourceData.lastName]
3  .filter(Boolean)
4  .join(' ')

This is great, it now works with your requirement of cleanly displaying the first and last name of the user on a screen.

Though, it's on one condition, that all these modifications are right after the original source.

 1const sourceData = fetchUserInformation()
 2sourceData.displayName = [sourceData.firstName, sourceData.lastName]
 3  .filter(Boolean)
 4  .join(' ')
 5
 6/// somewhere after 100 lines of code
 7
 8sourceData.displayNameLong = [
 9  sourceData.firstName,
10  sourceData.middleName,
11  sourceData.lastName,
12]
13  .filter(Boolean)
14  .join(',')

This though, will need a little more knowledge of the codebase to remember that you wrote another modifier statement somewhere down the line. Which, well to be fair is hard to keep in mind forever and if someone new is working on this codebase, they wouldn't know that it existed unless they read the whole thing or searched for it.

This is where transforms come into picture. They aren't some magical concept it's a simple function that modifies the original source.

 1const sourceData = fetchUserInformation()
 2const userDetails = withDisplayNames(sourceData)
 3
 4function withDisplayNames(source) {
 5  const joinNames = (...arr) => arr.filter(Boolean).join(' ')
 6
 7  const displayName = joinNames(source.firstName, source.lastName)
 8  const displayName = joinNames(
 9    source.firstName,
10    source.middleName,
11    source.lastName
12  )
13
14  return {
15    ...source,
16    displayName,
17    displayNameLong,
18  }
19}

Now, no matter where you use userDetails the origin point is still on the line with const userDetails = withDisplayNames(sourceData) thus making it very clear that the withDisplayNames function is doing something with the data. In the example above, we also made a clone of the original object to avoid tampering the original source of data. This is called derived data and is a simple concept that's used to maintain purity and immutability in the code.

Because it is now using a tranform, you can add in more overlapping transformations, I could add something for profilePic fetching for example

 1const sourceData = fetchUserInformation()
 2const userDetails = withProfilePicUrl(withDisplayNames(sourceData))
 3
 4function withProfilePicUrl(source) {
 5  let profilePic = `https://cloud.image.provider.com/${source.imagePath}/modify?resolution=240_240`
 6  return {
 7    ...source,
 8    profilePic,
 9  }
10}

Similarly, the mod's are copied around, the functionality is now contained and re-usable where needed. You obviously don't want to overdo this but what you've just gone through is the very basics of how functional programming is done. Each function defines it's expected input and you get an expected output.

The benefits of doing this is the following

  • More confidence in what you write
  • Overall, more data oriented code than code tied to the UI
  • Easier to reference and re-use