loading...

Conditional rendering in React with catamorphisms.

vageez profile image Angelo Updated on ・4 min read

Do you ever look at your React components and think to yourself, there has got to be a better way to handle these conditions inside my components.

When I was first introduced to functional programming. One of the "rules" imposed was to never use if / else / else if.

This presented a huge issue for me. How can I manage this. Also, how can I manage this in my React Components?

Lets first look at an example of what I am talking about.
Below is an example of checking a variable and then returning the correct component.

The same result can also be achieved using a switch statement.


import React from 'react'
import Doberman from './Doberman'
import Chihuahua from './Chihuahua'
import BullDog from './BullDog'

const Dog = breed => {

    if(breed === 'Doberman') {
        return <Doberman />
    } else if (breed === 'Chihuahua')
        return <Chihuahua />
    } else {
        return <BullDog />
    }

}

So, whats wrong with this?

In my opinion, its ugly.

It is not safe. breed can come back as undefined or better yet some other breed of dog.

Also, in this example we are adding logic to our UI components, which is challenging to test.

So lets talk about how Catamorphisms can helps us manage these conditions in a different way.

Catamorphism

Taken from Wikipedia
In functional programming, catamorphisms provide generalizations of folds of lists to arbitrary algebraic data types

For those new to functional programming, fold can also be referred to as reduce or aggregate.

Say our application needed to determine the breed of dog, and then render the corresponding component to its user. In order to implement a catamorphism we would need to identify all the breeds of dog we would expect.

Here is an example of our list, that will support a catamorphism.


import daggy from 'daggy'

const DogBreed = daggy.taggedSum('DogBreed', {
    Doberman : [],
    Chihuahua : [],
    BullDog : [],
    Unknown : []
})

Check out daggy here

Our application would need to have an initial state defined. Inside our initial state we would assign our dog breed. Lets have a look...


const INITIAL_STATE = {
    dog : {
        breed : DogBreed.Unknown
    }
}

Since our application has not yet loaded, and we do not know what our dog breed is, we create an Unknown breed.

At some point in our applications lifecycle we would set our dog's breed.

Lets have a look at this example of setting our dog's breed using this super cool FP library Pratica.


import { Ok, Err } from 'pratica'
import daggy from 'daggy'

const DogBreed = daggy.taggedSum('DogBreed', {
    Doberman : [],
    Chihuahua : [],
    BullDog : [],
    Unknown : []
})


// DogBreed.is 
// A useful built in type check when using daggy.
const isValidBreed = breed => DogBreed.is(breed) ? Ok(breed) : Err()

// Safe function with no side effects. 
export const getBreed = dog => Ok(dog)
    .chain(dog => Ok(dog.breed))
    .map(breed => breed === 'Doberman' ? DogBreed.Doberman : breed)
    .map(breed => breed === 'Chihuahua' ? DogBreed.Chihuahua : breed)
    .map(breed => breed === 'BullDog' ? DogBreed.BullDog : breed)
    .chain(isValidBreed)
    .cata({
        Ok: breed => breed,
        Err: () => DogBreed.Unknown
    })

Let me take a second to talk about what is going on here.
Im using the Ok monad to check our dog object.

  1. We pass our dog into our Ok monad

  2. Next step, we chain chain allows us to unwrap our Ok Monad.
    a. Then set another Ok monad to check for dog.breed.
    b. If dog.breed is undefined our Ok monad will return Err and will pass straight to our cata.Err where we set DogBreed.Unknown.

  3. We then pass the into a map. Map accepts the output of ourchain, our OK Monad.
    a. Map takes an OK monad unwraps it and checks it and then and wraps it back into ourOk monad
    b. We map over every possible breed type.
    c. If we find a match we set our breed.
    d. If not we return type to our next map.

  4. Our last check, .chain(isValidBreed).
    a. Why do we need this? If the breed is not one we are expecting, we need to handle that case and defer to Err() which will default to DogBreed.Unknown
    b. We chain the result of our above maps into a function isValidBreed
    c. isValidBreed does a check on the breed. If it is of type DogBreed, we return an Ok monad with the breed. If not, we return Err()

Great.

Here, we show calling our safe getBreed functions that implements pratica.


// a contrived example of updating our state...
const dog = { breed: 'Chihuahua' }

const state = {
    dog : {
        breed : getBreed(dog) // DogBreed.Chihuahua
    }
}

We are now ready to see this in action.
Lets remember the goal is using an alternative to if / else in our react components.

We will be passing breed to our Dog component. breed is now an instance of our daggy DogBreed. Meaning we can apply a catamorphism (cata) to it. The cata will resolve to its current type.


import React from 'react'
import Doberman from './Doberman'
import Chihuahua from './Chihuahua'
import BullDog from './BullDog'

// Lets remember here that breed is the result of `getBreed(dog)` which is a List we can apply a catamorphism on.
const Dog = breed => breed.cata({
    Doberman  : () => <Doberman />,
    Chihuahua : () => <Chihuahua />,
    BullDog   : () => <BullDog />,
    Unknown   : () => <div>{'Unknown breed'}</div>,
})

Daggy also supports the passing of parameters, which can also be quite interesting. Check out daggy here.

Posted on by:

vageez profile

Angelo

@vageez

Senior Full Stack Developer

Discussion

markdown guide