Back

Contextual Helpers are easier to write but ...

Clickbait title for the win.

Anyway, what's this about?

I've only been a programmer for about 4.5 years (as of writing this post) and no doubt there that we all grow after making enough mistakes that we reflect upon and correct.

True for everything in life but I'm going to keep it limited to programming

Contextual Helpers

Or, helpers that are limited to a context or more specifically business logic. These are small functions that are very dependent on the data structure that's specific to the application you are building. To simplify, let's say I have a job listing app and I'm working on the page where these are to be visualized.

interface JobListing {
  role: string
  joiningDate: Date
  name: string
  // ... remaining fields
}

Assuming , we have the above structure / type for whatever I receive from the backend, I might have a formatter for the response to model it for consistency across the app or I might just use these fields as is. Though, often I'll need to write helpers that are very specific to this data.

For example, let's say I need to show all the Software Developer roles with the hex #18181b then what?

import { Text } from '@components'
import { standardDate } from '@utils/date'

const getPositionStyledText = (role) => {
  let color = '#000'
  switch (role.toLowerCase()) {
    case 'software developer': {
      color = '#18181b'
      break
    }
  }

  return {
    style: { color },
  }
}

const JobRoleText = ({ role }) => {
  const textStyle = getPositionStyledText(role)
  return <Text style={textStyle.style}>{role}</Text>
}

const JobListingCard = ({job})=>{
    return <>
        <p>{job.name}</p>
        <p>{standardDate(job.joiningDate)}</p>
        <JobRoleText role={job.role}>
    </>
}

Now we have 2 things that are very specific to this project, the component and the helper for the component, you can move the function inside but then that function would get redefined every time and will need to be inside a useCallback to avoid that so it's easier to just have it outside.

Back to the point, This is a bloc helper, or a business logic helper and these are often just limited to the app you write them for, moving them to other apps might need a lot of modification and so these are left alone and people generally start from scratch

Generic Helpers

As the name suggests, these are more geared towards being reused and don't really have business logic tied to them. Thing is, these are a little harder to write compared to the contextual one's because here you have to decide and design the API of the helper in a way to make it generic enough to be reused.

I'm going to give a small example and reuse the above styling helper again but this time written with a more generic API.

import { Text } from '@components'

type ColorMap = Record<string, string>

function createColorMap(definitions: ColorMap, defaultColor: string): string {
  return toMatch => {
    if (!colorMap[toMatch]) return defaultColor

    return colorMap[toMatch]
  }
}

// and the usage would look like so
const roleColor = {
  'software developer': '#18181b',
}

const roleColorMatcher = createColorMap(roleColor, '#000')

const JobRoleText = ({ role }: { role: string }) => {
  const textColor = roleColorMatcher(role.toLowerCase())
  return <Text color={textColor}>{role}</Text>
}

before we go to the explanation

  1. The above is not the best API to write a colorMap, it can be improved a ton more
  2. This is an example, take it like one!

To the mounta.. explanation.

We asked 3 things, which will help you create most of the helpers you write.

  1. What is base operation of helper?
  2. How do I get the data?
  3. Can it even be made generic?

Let's start with the 3rd question first, because that's important to understand.

Can it be generic?

Not all helpers can be made generic, and even if they can be, the API of the helper might not be as simple as the one with the context. What do I mean by that?

Back to examples, let's say I have a few cases where the provider can be an organization or a middleman or a startup each having the type = 1 | 2

function getJobNameWithProvider(job) {
  if (job.type === 1) {
    if (job.org.type === 'startup')
      return `${job.org.name} | ${job.name} (Startup)`
    return `${job.org.name} | ${job.name}`
  }

  if (job.type === 2) return `${job.poster.name} | ${job.name}`
}

I could also use a switch statement to add all this to a single string but for now, this is complicated enough to explain what I'm trying to.

Here if I do make a generic helper, I'll be picking random fields based on conditions and if I do make a generic helper it would look pretty much like the createColorMap function but it's the API that's troublesome.

function createConditionalPicker(pickerMap) {
  return (passerObj, condition) => {
    if (pickerMap[condition]) return pickerMap[condition](passerObj)
    return null
  }
}

// and usage would look like so
const jobNamePicker = createConditionalPicker({
  1: job =>
    job.org.type === 'startup'
      ? `${job.org.name} | ${job.name} (Startup)`
      : `${job.org.name} | ${job.name}`,
  2: job => `${job.poster.name} | ${job.name}`,
})

const job = {
  type: 1,
  org: {
    name: 'BarelyHuman',
    type: 'startup',
  },
}

const jobName = jobNamePicker(job, job.type)
// BarelyHuman | undefined (Startup), since I haven't handled null cases above

You can technically use the createConditionalPicker to even create the color mapper above, but that's not the point.

So, here we have 2 things,

  1. You are still writing your own business logic
  2. The API needs to be explained well to a new developer joining the project.

Looks like I almost wrote a monad though...

back to the concept, in cases like these it's easier to read and modify the original contextual helper than trying to make it generic. This will come with practice so you'll be making un-needed generic helpers quite often.

How do I get the data?

The 2nd question dictates how you design the API.

The above 2 helpers were curried functions since the setup data would be same and it'd make no sense to resend the entire object again and again when it's reference could be used and thus the returned function takes in the parameters that would actually access the reference point

If you are going to be working with different data every time, then you are better off with simpler functions.

If you are working with modifications on data, then we would need helpers that allow pipes, for example the above name picker could be written with something like @useless/asyncPipe

import asyncPipe from '@barelyhuman/useless/asyncPipe'

const jobDetails = await asyncPipe(
  async () => await getJobDetails(jobId),
  async job => {
    if (job.type === 1) {
      if (job.org.type === 'startup')
        job.jobNameWithProvider = `${job.org.name} | ${job.name} (Startup)`
      else job.jobNameWithProvider = `${job.org.name} | ${job.name}`
    }
    if (job.type === 2)
      job.jobNameWithProvider = `${job.poster.name} | ${job.name}`

    return job
  }
)

here the asyncPipe is the generic helper and we aren't really creating a generic helper for the job name, instead we make the modifications on the source data, which is how I would be doing the jobName field anyway but had to think of something simple to explain the 3rd question.

Now, people would ask, why would you write these functions inside a pipe? Good question.

The point of using a pipe to make sure the structure is modifiable, because the pipe assume a set of data to be passed down at all times. This isn't close to the original functional programming pipe but more to how coffeescript implements pipes.

The above in production code looks like this.

async function addProviderNameToJob(job) {
  if (job.type === 1) {
    if (job.org.type === 'startup')
      job.jobNameWithProvider = `${job.org.name} | ${job.name} (Startup)`
    else job.jobNameWithProvider = `${job.org.name} | ${job.name}`
  }
  if (job.type === 2)
    job.jobNameWithProvider = `${job.poster.name} | ${job.name}`

  return job
}

const jobDetails = await asyncPipe(
  async () => await getJobDetails(jobId),
  addProviderNameToJob
)

At this point, the addProviderNameToJob is optional, I can remove it and add it anywhere in the pipe and still expect the same result because you'd conceptually pass the same job down the pipe. The asyncPipe from @useless isn't tightly tied to a source for other reasons but based on functional concepts. You'd have one source and multiple sinks for that source.

The sinks are what consume the source, make modifications to it and return it. I can add another modification in the middle if it does the same thing, consumes the source, modifies it and returns it

async function addProviderNameToJob(job) {
  if (job.type === 1) {
    if (job.org.type === 'startup')
      job.jobNameWithProvider = `${job.org.name} | ${job.name} (Startup)`
    else job.jobNameWithProvider = `${job.org.name} | ${job.name}`
  }
  if (job.type === 2)
    job.jobNameWithProvider = `${job.poster.name} | ${job.name}`

  return job
}

async function addBarelyHumanToJob(job) {
  job.isFromBarelyHuman = true
  return job
}

const jobDetails = await asyncPipe(
  async () => await getJobDetails(jobId),
  addBarelyHumanToJob,
  addProviderNameToJob
)

Pretty stupid for an example, but I hope you get the point, my business logic can be separated in chunks and still be added or removed at will.

Don't be fooled, you can do all that without even using asyncPipe but it's a little more structured for my mental model.

What is base operation of helper?

Last question, what is the base operation. The base operation for the color mapper was to compare a string to another string (switch cases or if case) which can be moved into comparing a value in map.

You basically move out the base operation and write that into a generic function and then move the data dependent decisions out to the developer using the API.

Combine this with the answers to the other 2 questions and you'll have an helper design in your head or on the paper. Then run a few tests and voila, generic helpers!!

Conclusion

It's always going to be easier to write business logic specific helpers but if something can be split into a generic helper which you see happening in most projects that you write, spend a little more time and make a generic helper out of it.

Do keep in mind that not everything needs to be generic, some things are easier to read and modify when left with their context.