4

I'm trying to wrap my head around avoiding state using JavaScript functional programming techniques. I have a solid grasp of many basic fp techniques such as closures, currying, and so on. But I can't wrap my head around dealing with state.

I'd like to know how someone creating a functional program would implement the following very simple application:

A user clicks a button in the browser (jQuery implementation is fine). The value onscreen should increment by 1 each time the user clicks the button.

How can we do this without mutating state? If mutating state is necessary, what would be the best approach from a functional perspective?

  • 1
    The functional approach is not to *avoid* state, but to make state explicit. And on some level of abstraction we cannot even do that, because the DOM is simply mutable and user interaction is stateful. – Bergi Oct 30 '19 at 20:22
  • I understand that the DOM is mutable and a function with side effects is necessary to update the DOM. So there is some give and take here for sure. How would you make state explicit in this situation? – elemental13 Oct 31 '19 at 22:59
  • Have a look at [functional reactive programming](https://en.wikipedia.org/wiki/Functional_reactive_programming) or the [Elm architecture](https://guide.elm-lang.org/architecture/) – Bergi Nov 01 '19 at 04:22

2 Answers2

1

As you can see, a counter in non-fp mode is simply stateful. It holds the state so that can increment or decrement it accordingly to their api.

const createCounter = () => {
  let value = 0;
  
  return {
    get value() {
      return value;
    },
    increment() {
      value = value + 1;
    },
    decrement() {
      value = value - 1;
    },
  };
};


const counter = createCounter();
console.log('initial value', counter.value);

counter.increment();
counter.increment();
console.log('value after two increments', counter.value);


counter.decrement();
console.log('value after one decrement', counter.value);

The functional way of building a counter is to let the consumer provide the state. The functions only know how to mutate it:

const incrementCounter = counter => counter + 1;
const decrementCounter = counter => counter - 1;

const value = 0;
console.log('initial value', value);

const valueAfterTwoIncrements = incrementCounter(
  incrementCounter(value),
);

console.log('value after two increments', valueAfterTwoIncrements);


const valueAfterOneDecrement = decrementCounter(valueAfterTwoIncrements);

console.log('value after one decrement', valueAfterOneDecrement);

The advantages of this approach are almost countless, functions are pure and their output deterministic so testing is very easy etc.


Q&A:

  1. "Let the consumer provide the state": The functions (inc/dec) don't work with their own state, they take it as argument and return a new version of it. Try to think of a redux reducer, they only embed the logic to change the state... but ultimately, the state is passed as argument.
  2. "where the values continue to get incremented/decremented": The state do never change, this is called immutability, the pure functions will always return a new copy of it so that if you want you have to store it somewhere else.

Working with the DOM

Separate View Layer from the actual Business Logic Layer

As you can see on the following example, the view is data-unaware, the dom is only used to render or trigger ui events, while the actual business logic (current state value and how to inc/dec) is being held on a different and well separated layer. The orchestration layer is ultimately used to bind those two layers together.

/***** View Layer *****/
const IncBtn = ({ dispatch }) => {
  dispatch({ type: 'INC' });
};

const DecBtn = ({ dispatch }) => {
  dispatch({ type: 'DEC' });
};

const Value = ({ getState }) => {
  document.querySelector('#value').value = getState();
};


/***** Business Logic Layer *****/
const counter = (state = 0, { type }) => {
  switch(type) {
    case 'INC':
      return state + 1;
      
    case 'DEC':
      return state - 1;
    
    default:
      return state;
  }
};


/***** Orchestration Layer *****/
const createStore = (reducer) => {
  let state = reducer(undefined, { type: 'INIT' });
  
  return {
    dispatch: (action) => {
      state = reducer(state, action);
    },
    getState: () => state,
  };
}


(() => {
  const store = createStore(counter);
  // first render
  Value(store);
  
  document
    .querySelector('#inc')
    .addEventListener('click', () => {
      IncBtn(store);
      
      Value(store);
    });
  
  document
    .querySelector('#dec')
    .addEventListener('click', () => {
      DecBtn(store);
      
      Value(store);
    });
})();
<button id="inc">Increment</button>
<button id="dec">Decrement</button>
<hr />

<input id="value" readonly disabled/>
Hitmands
  • 13,491
  • 4
  • 34
  • 69
  • Hm, while it is technically correct to store state in the call stack you didn't address the DOM part, namely how to wire this approach with the DOM. –  Oct 30 '19 at 20:46
  • I'm okay with this answer not mentioning the DOM aspect if it clearly shows how state should be updated. I'm not sure what you mean by "let the consumer provide the state," though. I also don't see where the value actually continues to get incremented. It can go from 0 to 2, but this won't take it from 2 to 4. – elemental13 Oct 31 '19 at 22:34
  • Your DOM example is not functional because your store is impure. Can you make your example work without mutating the state within the store? Hint, it is possible and the resulting code is pithier. – Aadit M Shah Nov 01 '19 at 14:47
  • Not quite sure on what do you mean by _your store is impure_, what really needs to be pure is your reducer function... the store is stateful by definition and you can look at how [redux](https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L245) implemented it. – Hitmands Nov 01 '19 at 20:54
  • Redux is a bad example. It unnecessarily complicates things with actions, reducers, and dispatching. Consider your example. When the user presses the `inc` button, the `IncBtn` function is called which dispatches an action to the reducer, which returns the incremented value to the store, which updates its internal state. After that you have to call the `Value` function manually to render the updated state. This leads to code bloat. What you were able to do in 68 lines of code, I was able to do in 19 lines of code using recursion. Things are all over the place which makes maintenance difficult. – Aadit M Shah Nov 02 '19 at 04:44
  • from pair to pair, this doesn't really look like a constructive approach. I asked you to clarify why you argued "_Your DOM example is not functional because your store is impure. _" and you answered with redux's boilerplate, turning this into a p.n.s size challenge... While your example has less lines of code, someone could argue that having the render embedding the transition logic is not really fp because fns should have one single responsibility. Mine is closer to the real world in which the boilerplate would be 1 line of code, since you would delegate it to a framework. – Hitmands Nov 02 '19 at 08:16
  • It's not functional because your store is impure. Your store is impure because the `getState` function breaks referential transparency. It returns different results at different times when given the same input. – Aadit M Shah Nov 03 '19 at 03:08
  • On the contrary, I think it makes perfect sense for the `render` function to embed the transition logic. The `render` function takes some input and generates some HTML. The transition functions are a part of that HTML. In fact, React does the same thing. In React, you add event handlers in the `render` function. – Aadit M Shah Nov 03 '19 at 03:15
1

Here is how you would implement a simple counter app without mutating anything except for the DOM.

const h1 = document.querySelector("h1");

const [decrement, reset, increment] = document.querySelectorAll("button");

const render = count => {
    h1.innerHTML = count; // output
    decrement.onclick = event => render(count - 1); // -+
    reset.onclick     = event => render(0);         //  |-- transition functions
    increment.onclick = event => render(count + 1); // -+
};

render(0);
//     ^
//     |
//     +-- initial state
<h1></h1>
<button>-</button>
<button>Reset</button>
<button>+</button>

This is an example of a Moore machine. A Moore machine is a finite-state machine. It consists of three things.

  1. The initial state of the machine. In our case, the initial state is 0.
  2. A transition function, which given the current state and some input, produces a new state.
  3. An output function, which given the current state, produces some output.

In our case, we combined the transition function and the output function into a single render function. This is possible because both the transition function and the output function require the current state.

When the render function is provided the current state, it produces some output as well as a transition function which when provided some input, produces a new state and updates the state machine.

In our case, we divided our transition function into multiple transition functions which share the current state.


We can also use event delegation to improve performance. In the example below we only register one click event listener on the entire document. When the user clicks anywhere in the document, we check whether the target element has an onClick method. If it does, we apply it to the event.

Next, in the render function instead of registering separate onclick listeners for each button, we store them as regular methods named onClick (different case). This is more performant because we're not registering multiple event listeners, which would slow down the app.

const h1 = document.querySelector("h1");

const [decrement, reset, increment] = document.querySelectorAll("button");

const render = count => {
    h1.innerHTML = count;
    decrement.onClick = event => render(count - 1);
    reset.onClick     = event => render(0);
    increment.onClick = event => render(count + 1);
};

render(0);

document.addEventListener("click", event => {
    if (typeof event.target.onClick === "function") {
        event.target.onClick(event);
    }
});
<h1></h1>
<button>-</button>
<button>Reset</button>
<button>+</button>
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • I like this solution and the reference to the Moore machine. The render() function is also reminiscent of React so cool solution. While this solution isn't technically altering state in the logic, it is still relying on that state being stored in the DOM. Not necessarily a terrible thing, but is that ideal? – elemental13 Oct 31 '19 at 22:56
  • It's not storing any state in the DOM. That would imply that you retrieve the state from the DOM when you're updating it. However, you do need to update the DOM in order to create a web app. That's unavoidable. – Aadit M Shah Nov 01 '19 at 02:02
  • 2
    The semblance to React is not superficial. React essentially models a Moore machine, albeit one that's distorted by side effects. I'm not a big fan of React. Facebook had the right idea but they messed up the implementation. React is more functional than jQuery but it's not truly functional. Even React hooks aren't truly functional. If you want a truly functional framework for creating web apps then take a look at Elm. – Aadit M Shah Nov 01 '19 at 02:11
  • 1
    @AaditMShah Well technically the state is stored in a variable scope that is closed over by functions that are stored in the DOM, but you cannot really avoid that and imo the encapsulation this provides is actually better than storing the state in the global scope. The import part is however that it does not *read* any state from the mutable DOM. – Bergi Nov 01 '19 at 04:32
  • If you'd additionally provide a diffing algorithm or, even better, a way to incrementally update changes without a diff than React could be ditched for most projects.. –  Nov 01 '19 at 10:15
  • The idea of using recursion to avoid storing the state in a variable comes at the cost of _(a)_ call stack growth and _(b)_ reassigning event listeners at every render. – Hitmands Nov 01 '19 at 21:02
  • @Hitmands There's no call stack growth since the event listeners are invoked asynchronously. – Bergi Nov 01 '19 at 21:20
  • right, recursion is async here! Thank for pointing that out :) – Hitmands Nov 01 '19 at 21:22
  • 1
    @Hitmands You don't even have to reassign event listeners at every render if you use event delegation. Here's an example. https://jsfiddle.net/aaditmshah/dj8c45zt/. Now, you only have one event listener, and on each render you update a property on the DOM object, which is a lot faster than adding an event listener. – Aadit M Shah Nov 02 '19 at 04:18
  • Could you dive into more details on how e.d helped on your fiddle? I still see listeners being attached at every render... Could you also put the snippet on SO, external links might cease to exist... – Hitmands Nov 02 '19 at 08:18
  • @Hitmands Those aren't event listeners. For example, `decrement.onclick` is an event listener. However, `decrement.onClick` is not. They are just regular functions being stored on an object. If you remove the `document.body.addEventListener` then none of the buttons would work. By the way, React does the same thing. It stores functions on DOM elements and then uses event delegation to call the functions. The advantage is that you may have thousands of buttons with thousands of functions attached to them. However, you only have one event listener. Listeners are more expensive than functions. =) – Aadit M Shah Nov 02 '19 at 09:06
  • yup, didn't notice the different casing! – Hitmands Nov 02 '19 at 14:36
  • @scriptum, I think the edit adds more indirection to an otherwise simple answer – Mulan Sep 13 '20 at 17:52
  • @Thankyou Hm, it is up to Aadit then. Just think that event delegation is the crucial detail of such a "pure" solution. –  Sep 13 '20 at 18:18
  • 1
    @scriptum Moved event delegation to the end of the answer, and added an explanation as to why it's better. – Aadit M Shah Sep 14 '20 at 05:37