DEV Community

Cover image for Declarative JavaScript
Sultan
Sultan

Posted on • Updated on

Declarative JavaScript

Dipping your toes into the world of functional programming can sometimes feel overwhelming. The transition from understanding its core concepts to actually applying them in real-world scenarios can be challenging. If you're familiar with some functional programming techniques but unsure when and how to use them, you're in the right place. In this article, we'll discuss some key concepts and their practical uses, and also explore how declarative programming is connected to functional programming.

Imagine writing JavaScript code without using variables, loops, or logical constructs.

This might sound bold, right? But how often do you find yourself writing code like this to toggle the visibility of a component in JSX?

<div>
  {hasComments ? <Comments/> : null}
</div>
Enter fullscreen mode Exit fullscreen mode

We can rewrite it in a more declarative way:

<div>
  <Comments visible={hasComments}/>
</div>
Enter fullscreen mode Exit fullscreen mode

As noted, this version is more declarative. Although we still use the conditional statement if internally, the goal is to abstract it from the main code.

const Comments = ({visible}) => {
  if (!visible) return null
  ...
}
Enter fullscreen mode Exit fullscreen mode

The visible property prompts us to write code in a declarative manner, even though the component itself is partially implemented in an imperative style.

Let's take a look at the logical operator switch, which isn't just closely related to if, but is also a favorite among many developers:

const App = () => {
  const role = useUserRole()
  let Component
  switch(role) {
    case 'ADMIN': {
      Component = AdminView
      break
    }
    case 'EDITOR': {
      Component = EditorView
      break
    }
    case 'USER': {
      Component = UserView
      break
    }
    default: {
      Component = GuestView
      break
    }
  }

  return (
    <main>
      <NavBar/>
      <Component/>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

Perhaps you’ve already guessed where I'm heading with this, especially since a similar approach is implemented in React Router.

const App = () => (
  <main>
    <NavBar/>
    <Switch test={useUserRole()}>
      <Case when='ADMIN' use={AdminView}/>
      <Case when='EDITOR' use={EditorView}/>
      <Case when='USER' use={UserView}/>
      <Otherwise use={GuestView}/>
    </Switch>
  </main>
)
Enter fullscreen mode Exit fullscreen mode

Let’s consider this code from the perspective of functional programming, where HTML/JSX tags are treated as functions that produce HTML output and tag attributes act as function input parameters.

// it will return HTML: <main id="app">Hello World<main/>
main({id: 'app', children: 'Hello world!'})

// The second parameter can be used as a children attribute
main({id: 'app'}, 'Hello world!')
Enter fullscreen mode Exit fullscreen mode

Drawing from the concept above, let's express the JSX code through function sets. It's pertinent to note that switch/case are reserved JavaScript keywords, so we'll add underscores:

const app = () => (
  main(
    navbar(),
    switch_({test: useUserRole()},
      case_({when: 'ADMIN', use: AdminView}),
      case_({when: 'EDITOR', use: EditorView}),
      case_({when: 'USER', use: UserView}),
      otherwise({use: GuestView}),
    )
  )
)
Enter fullscreen mode Exit fullscreen mode

As you can see, declarative code doesn't always mean HTML or JSX. In JavaScript, we can use functions to represent the syntax of these languages. According to JS conventions, functions should represent actions and usually start with a verb, such as find or setTitle. However, in this case, functions can also represent an entity. For example, an SQL query can be written like this:

// a function composition
query(
  select('name', 'email', 'country'),
  from('users'),
  where({age: less(21)}),
  groupBy('country'),
)

// or as a chaining function like Promise
select('name', 'email', 'country')
  .from('users')
  .where({age: less(21)})
  .groupBy('country')

// SELECT name, email, country FROM users WHERE age < 21 GROUP BY country
Enter fullscreen mode Exit fullscreen mode

This way, I want to draw a connection between declarative and functional programming. I believe that functional programming does not intentionally pursue declarativeness; rather, it is a result of extensively using functions. It's worth mentioning that functional programming is not solely about the DRY principle, which involves extracting repetitive code into separate functions. At its core, functional programming focuses on crafting universal functions that are both highly composable and versatile in application.

Now we can take another look at the switch/case and represent them as a plain function:

const selectComponent= ({test, cases, defaultValue}) => {
  const found = cases.find([value] => test === value)
  return found?.at(1) || defaultValue
}

const App = () => {
  const role = useUserRole()
  const Component = selectComponent({
    test: role,
    cases: [
      ['ADMIN', AdminView],
      ['EDITOR', EditorView],
      ['USER', UserView],
    ],
    defaultValue: GuestView,
  })

  return (
    <main>
      <NavBar/>
      <Component/>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

The current implementation falls short in terms of flexibility and composability with other functions. To enhance its utility, we could transform the selectComponent function into a higher-order function and break it down into more granular functions.

const select = (...fns) => value => fns.reduce(
  (found, fn) => found || fn(value), null
)

const when = (test, wanted) => value => {
  const matched = typeof test === 'function' ? test(value) : test === value
  return matched && wanted
}

const selectComponent = select(
  when('ADMIN', AdminView),
  when('EDITOR', EditorView),
  when('USER', UserView),
  () => GuestView,
)

const Component = selectComponent('EDITOR') // -> EditorView
Enter fullscreen mode Exit fullscreen mode

If you're only somewhat familiar with the concept of curried functions, this serves as a solid illustration of their practical use. While a curried function resembles a standard function, its execution can be postponed until all its parameters have been supplied. This capability not only facilitates function composition, but also renders the code more declarative and understandable.

We have the capability to craft various iterations of the when function, all the while ensuring that the primary select function remains intact and requires no alterations. Below is an example that demonstrates how to support lazy loading of components:

import {Suspense, lazy} from 'react'

const when = (test, path) => value => (
  test === value && lazy(() => import(path))
)

const selectComponent = select(
  when('ADMIN', './admin-view'),
  when('EDITOR', './editor-view'),
  when('USER', './user-view'),
  () => GuestView,
)

const App = () => {
  const role = useUserRole()
  const Component = selectComponent(role)

  return (
    <main>
      <NavBar/>
      <Suspense fallback={<div>Loading...</div>}>
        <Component/>
      </Suspense>
    </main>
  )
}
Enter fullscreen mode Exit fullscreen mode

If we take a much closer look at the initial version of the selectComponent function, it becomes clear that it is not possible to extend its functionality without making changes. With this approach, we can break down the code into smaller functions, each with a specific task in mind. This improves the test coverage experience and minimizes the future changes impacting multiple sections of the code. If we ever need to expand the functionality, we can simply add a new function and integrate it with the existing functions rather than rewriting the main function each time.

const between = (min, max) => n => (
  min >= n && n <= max
)

// range of values
const toGrade = select(
  when(val => val > 90, 'A'),
  when(between(80, 89), 'B'),
  when(between(70, 79), 'C'),
  when(between(50, 69), 'D'),
  () => 'F',
)

const grade = toGrade(81) // -> 'B'
Enter fullscreen mode Exit fullscreen mode

In the next post we’ll take a detailed look at this technique using the example of creating Redux reducers. We will learn how to replace the try/catch approach and continue the discussion on functional programming.

const authReducer = createReducer(
  initialState,
  on('SIGN_IN', signIn),
  on('SIGN_OUT', signOut),
  on('SIGN_OUT', clearCookies),
)
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
pengeszikra profile image
Peter Vivo

Very clever functional solution, grat!