paint-brush
Forms of Composition in JavaScript and Reactby@sambernheim
959 reads
959 reads

Forms of Composition in JavaScript and React

by Sam BernheimSeptember 6th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In functional programming, the basic unit for composition is functions and larger functions are built by connecting and combining smaller ones. Composition relies on recursion to build larger abstractions from smaller abstractions but with each abstraction layer as the same type as all the others. ReactJS uses components as a unit of composition in JavaScript and ReactJS. The idea is similar to legos, legos that can be joined and yield another lego that can continue to be built on and attached to others. It uses an imperative style, while it isn't necessarily clean code, it is simple enough to get the desired result.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Forms of Composition in JavaScript and React
Sam Bernheim HackerNoon profile picture

One of the core ideas in functional programming is composition: building larger things from smaller things. The canonical example of this idea should be familiar with legos.

Multiple legos can be joined and yield another lego that can continue to be built on and attached to others. In functional programming, the basic unit for composition is functions and larger functions are built by connecting and combining smaller ones.

When asked how to handle a particular scenario, edge case, or requirement, a functional programmer will nearly always answer: 'with a function'.

Object-oriented concepts like factories, strategy mangers, or polymorphism don't have an equivalent in the functional paradigm. Functional programming has its own key concepts, composition is one.

A (quick) Aside

One distinction between functional and object oriented programming is best seen in the difference between

circle.area()
and
area(circle)
. In the first version - a more object oriented style -
area
is a method that exists on the
Circle
class and can only be called on
Circle
instances. In the second,
area
is a function that accepts an object. Unlike the object oriented version, it can act on any object that conforms to the type expected by
area
.

This illustrates a core difference between the two paradigms. In an object oriented world, data and functionality are coupled -

area
is a function on instances of
Circle
objects limited to objects created by that class. In functional programming, data and functionality are decoupled -
area
is a function that can act on any object that has the required properties.

While object oriented patterns have interfaces, inheritance and other mechanisms for sharing behavior like calculating the area of various shapes, it can often feel like you're standing in an abandoned abstract factory churning out reverse proxies* 🙃

*I've never written an abstract factory and this is just a cheeky line to maintain engagement. Like all things, OO is another tool to leverage when needed.

Forms of Composition

Functions are not the sole unit of composition and the principle extends beyond the domain of functional programming. ReactJS uses components as a unit of composition. Hooks too like

useState
are another unit. If you're really focusing, you may notice that hooks are just regular functions which is why they are great for composition.

Its possible to build larger components from smaller components, and write custom hooks that extend the capability of existing hooks.

Composition relies on recursion to build larger abstractions from smaller abstractions but with each abstraction layer as the same type as all the others. Once a compositional unit like functions or components exist, you immediately get a compositional model that allows for building high level abstractions very quickly for free. Each layer of abstraction is fundamentally the same type of thing as all the other layers.

An Example of (Functional) Composition

Let's begin with three functions.

const toUpperCase = str => str.toUpperCase();
const appendExclamationPoints = str => str + '!';
const split = str => str.split('');

Often code takes the output of one function and uses it as the input to another. This is the idea of a pipeline. Data in, data out.

split(appendExclamationPoints(toUpperCase('hello world'))) 
// ["HELLO", "WORLD!"]

While the above works, it isn't the easiest to read. A simpler abstraction is a single function that can be invoked with some string passed as the parameter.

function appendExclamationPointAndSplitOnSpace(someString) {
    return (someString.toUpperCase() + '!').split();
}

appendExclamationPointAndSplitOnSpaceagic('hello world') 
// ["Hello", "World!"]

The above function, while meeting the requirements perfectly, isn't necessarily clean code. It uses an imperative style, specifying each operation to perform to get the desired result. While it may be simple enough to read and understand, a more declarative style would be even easier.

Functional programming can help simplify the above through a helper function called

compose
. Compose accepts an arbitrary number of functions, and returns a new function that runs each of the functions passed in such that the output of the previous functions is used as the input to the next.

const appendExclamationPointAndSplitOnSpace = compose(
    split, 
    appendExclamationPoints, 
    toUpperCase
);

appendExclamationPointAndSplitOnSpace('hello world') 
// ["Hello", "World!"]

Note that the functions execute in a right to left wrapping manner similar to the original example. That is,

split
invokes the result of
appendExclamationPoints
which invokes the result of
toUpperCase
. This results in a declarative style, with no direct function calls or references to the data and methods that exist on the data type. A new function is created that accepts the data and computes the result. Most importantly, we're able to build the new function from existing smaller functions that we already have or are trivial to write.

Composing functions requires adherence to the following intuitive rule. The output type of function A must match the input type of function B given that B runs with the output from function A. In a practical example, if a function that returns a number is composed with a function that expects a string, some unexpected errors might creep in.

Various implementations of

compose
can be found in Lodash, Redux, and other JavaScript libraries. Its certainly not magical, and can be written in under 10 lines.

Spotting Use Cases for Functional Composition

const resOne = foo(data);
const resTwo = bar(resOne);
const resThree = baz(resTwo);
const resFour = fizz(resThree);

// or something like

fizz(baz(bar(foo(data))))

Either of the above approaches can be replaced with a simpler version.

const myNewFunction = compose(fizz, baz, bar, foo);
const result = myNewFunction(data);

Writing this out in one line correlates nicely to a mental model of a linear flow for the call stack even if some of those functions may really be asynchronous.

Onto the real world. The below is a demonstration of composition in ReactJS.

A (somewhat) Real World Example

Below is a component that renders an

input
and an
h1
tag. The second word in the
h1
tag is controlled by the
text
state value.

function MyComponent() {
    const [text, setText] = useState('World');

    function handleChange(e) {
        e.preventDefault();
        const newText = e.target.value;
        setText(newText)
    }

    return (
        <div>
            <input 
                type='text' 
                onChange={handleChange} 
                placeholder='type something here' 
                value={text}>
            </input>
            <h1>Hello {text}</h1>
        </div>
    )
}

The

handleChange
function is just five lines (really two when discounting the signature, variable assignment, and closing curly brace 😜). The whole component is small and reusable but there's a more functional way of writing this...

A functional programmer would note that

handleChange
could cause a side effect (calling
e.preventDefault()
), increasing the overhead of the mental model we need to maintain.

The functional way to write this would be to split

handleChange
into smaller functions which would be connected with compose to build the final
handleChange
function.

One of these smaller functions will invoke

e.preventDefault()
(separating out its potential side effect from the rest of our otherwise pure code), another will extract the value from the event, and a third will update the state value. React kindly provides this third function for free through hooks or
setState
.

Below is an example.

function MyComponent() {
    const [text, setText] = useState('World');

    function preventDefault(e) {
        e.preventDefault();
        return e;
    }

    function getEventValue(e) {
        const newText = e.target.value;
        return newText;
    }

    const handleChange = compose(setText, getEventValue, preventDefault);

    return (
        <div>
            <input 
                type='text' 
                onChange={handleChange} 
                placeholder='type something here' 
                value={text}>
            </input>
            <h1>Hello {text}</h1>
        </div>
    )
}

While the total number of functions may have tripled, they are easier to test, reuse, and enhance through more composition. The same can be done for traditional class components swapping

setText
for
this.setState()
.

The original functional component has the below class equivalent.

class MyComponent extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            text: 'World'
        };

        this.handleChange = this.handleChange.bind(this);
    }

    function handleChange(e) {
        e.preventDefault();
        this.setState({ text: e.target.value });
    }

    return (
        <div>
            <input type='text' onChange={handleChange} placeholder='type something here' value={text}></input>
            <h1>Hello {this.state.text}</h1>
        </div>
    )
}

Refactoring to a compositional equivalent, the above class component would end up with this.

class MyComponent extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            text: 'World'
        };
    }

    function preventDefault(e) {
        e.preventDefault();
        return e;
    }

    function getEventValue(e) {
        return { text: e.target.value }
    }

    const handleChange = compose(this.setState, getEventValue, preventDefault);
    return (
        <div>
            <input 
                type='text' 
                onChange={handleChange} 
                placeholder='type something here' 
                value={this.state.text}>
            </input>
            <h1>Hello {this.state.text}</h1>
        </div>
    )
}

The only difference is that

getEventValue
returns an object in place of a value to meet the expected argument type for
setState
.

The Benefits of Composition

Composition scales irrespective to the problem space a function is being used to solve. Add in as much conditional logic as you like, throw in some asynchronous calls and error handling, sprinkle some data validation on but no matter what, the result is the same. As long the function types match up, composition is possible.

This often results in grokkable, pure, refactorable code that allows a strong mental model as simple as your functions. When some other practices are observed, functions make composition incredibly natural.

preventDefault
and
getEventValue
are completely decoupled from the component and generic to any event. They can be composed into a single more useful function that can be further extended.

export const preventDefaultAndGetValue = compose(
    getEventValue, 
    preventDefault
); 

If the return types for two functions don't match up but need to be composed, the solution is to often write another intermediary function to make the types match, almost like the adapter pattern.

Consider this kitchen sink example.

const preventDefault = (e) => {
    e.preventDefault();
    return e;
}

const getEventValue = (e) => e.target.value;

const validate = (str) => {
    const val = !!str ? str : 'You entered an invalid value. Try again';
    return val;
}

const appendExclamationPoint = (str) => str + '!';
const [text, setText] = useState('world');
const updateStateIfValid = compose(
    setText, 
    appendExclamationPoint, 
    validate, 
    getEventValue, 
    preventDefault
);

What about Compose?

The only function unexplored is

compose
. You, like our ever curious George, might be wondering what its implementation looks like.

I think it's a good challenge to write your own as an exercise, though its always a good idea to use one from a good library or other high quality source. Redux ships with a

compose
function as does Lodash and most functional programming or general utility libraries. You can always find various implementations with a quick search as well.

Conclusion

If you dig into functional programming, you inevitably come across terms like point free style, currying, and monoids (monads too but let's not go there quite yet 😉). These may be new and intimidating, but they're no more complex (and often simpler) than OO concepts like the strategy manager, encapsulation, or singletons. One of them was even conveyed in this article without a direct reference to the name.

As you may have realized, types are critical in functional programming; they act as proof that two functions are composable. Similarly, currying in functional programming is a mechanism for taking a multi-parameter function and turning it into a series of single parameter functions. Both of these help enable and enforce correctness in functional programming especially during composition.

preventDefault
purposefully returns the
event
rather then the result of
e.preventDefault();
. This ensures its output lines up with the input of
getEventValue
. Similarly,
getEventValue
returns a string which is the input to
validate
. A type system (and good languages) make composition and, more broadly, functional programming easy and safe, partially by ensuring function types line up.

JavaScript has some of the key features required for functional programming. React embraces its own compositional model using components as a primary unit of composition just like legos and functions. The react motto is the view is a function of state. This sounds like it could be a pure function...

Closing Thoughts

Thank you for reading. If you're interested in learning more about functional programming, I highly recommend any of the following videos, tutorials, or articles. I've used them all myself and have found them incredibly helpful.

- Mostly Adequate Guide To Functional Programming

- Elegant error handling with the JavaScript Either Monad - James Sinclair

Additionally, here are two great videos on other forms of composition specific to React.