DEV Community

Cover image for Stop managing state

Stop managing state

Mike Pearson on January 05, 2023

YouTube Have you ever written state management that looked like this? return ({ ...state, checked: !state.checked, }); Enter fullscr...
Collapse
 
brucou profile image
brucou

Mike, what you are looking for are functional optics. They compose very neatly, and you can view, set, traverse, and build pieces of state seamlessly. Monocle-ts and calmm-js are examples of such libraries. Ramda also offers functional lenses, which are part of the optics family. Now the difficulty is to find a nice introduction to the topic that is not overly academical...

Collapse
 
mfp22 profile image
Mike Pearson

Wow, I've never heard of that before. I'll have to look more into it, but based on the little bit of reading I just did, it seems really wordy. I'm looking for a minimal and simple way to generate state management logic. Well, I mostly implemented it already. Ramda lenses look more flexible possibly, but I would like to see how it looks in a real demo so I can compare it with state adapters.

Collapse
 
brucou profile image
brucou

Optics are commonly used in functional programming so few JS/TS programmers heard of them (generally the functional reactive programming folks). In a functional language such as Haskell, you can express many state modifications as one liner by composing optics. With TS/JS, truth is the resulting programs are hard to read.

Example from monocle-ts:

const employee: Employee = {
  name: 'john',
  company: {
    name: 'awesome inc',
    address: {
      city: 'london',
      street: {
        num: 23,
        name: 'high street'
      }
    }
  }
}

const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)

const employeeCapitalized = {
  ...employee,
  company: {
    ...employee.company,
    address: {
      ...employee.company.address,
      street: {
        ...employee.company.address.street,
        name: capitalize(employee.company.address.street.name)
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

turns to:

import { Lens } from 'monocle-ts'

const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')

const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)

assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true

Enter fullscreen mode Exit fullscreen mode

The chain of compose in the version using lenses takes some getting used to.

Other example from calmm-js:

const sampleTitles = {
  titles: [{language: 'en', text: 'Title'}, {language: 'sv', text: 'Rubrik'}, ...]
}

const textIn = language =>
  L.compose(
    L.prop('titles'),
    L.normalize(R.sortBy(L.get('language'))),
    L.find(R.whereEq({language})),
    L.valueOr({language, text: ''}),
    L.removable('text'),
    L.prop('text')
  )

L.get(textIn('sv'), sampleTitles)
// 'Rubrik'
Enter fullscreen mode Exit fullscreen mode

A little bit better to read due to the compose helper but still not ideal.

In terms of real-world example, Grammarly uses lenses (and functional reactive programming techniques) for its online application (cf. github.com/grammarly/focal) but that's closed source unfortunately...

Ramda lenses are probably the simplest to use and there are lots of articles online on how to use them for fun and profit. Example: itnext.io/a-beginners-guide-to-ram...

Collapse
 
amykhar profile image
amykhar

It's definitely an interesting idea. I've added it to my list of things to dive into further.

Collapse
 
jwp profile image
John Peters

I had always felt the React state patterns to be ridiculous. 25 years before React was invented, all Desktop applications handled state internally. Best of all it was easy to implement and easy to understand.

I never truly understood what state problems React was trying to solve.

Collapse
 
mfp22 profile image
Mike Pearson

Originally they were trying to solve inconsistent state by removing event handlers (or controllers) from their code. But it wasn't good enough because it didn't handle asynchronous changes, and that's where a lot of chaos entered in. There was also a translation problem where React devs thought the main point was just the ability to share state between components. That problem in React has been a huge distraction from the main problem of state management.

I'd suggest watching this starting at 10:20 until about 24:00. youtu.be/nYkdrAPrdcw

I'm curious though - do you know if how desktop applications handled events and state changes? If it's MVC, I guarantee they either had just as many bugs, or they didn't have applications as event-driven as modern web apps.

The point of my article isn't to say that state management is bad. Unidirectional/reactive state management is clean. The point is the obnoxious number of times I've implemented the same functionality in different apps.

Collapse
 
jwp profile image
John Peters

Very nice history lesson thanks. And yes I remember a few things now. One of them was this: 'Browers themselves use an event only architecture' every state change is a pub/sub event system. When I first started studying Reflux/Redux, I recognized the pattern as a pseudo event system. To me there was no reason to not use historical an proved event system.

When Redux came out, desktop apps where already using MVVM (~10 years), which is essentially a model based MVC design. Before there was Async/Await, there was BackGroundWorker support; which, was designed on the Async Delegate and threads concept. This solved the problem of cross thread update violations.

But as soon as Async/Await happened (~5 years before Javascript adopted it) it changed everything. Now, the work was farmed off to the I/O processor queue, no threads included. The Await automatically made sure the "captured data" was made available on the calling thread.

The caller would suspend the current stack on the Await, and resume only after the Async call was done. If states were a local non static variable then no state updates lost control ever.

The only gap left to plug was this 'parallel foreach patterns with no Async Await logic. Support for that was the ConcurrentBag container which could handle any List.

This solved all Async state concerns and much more, because each new Task (as it was named) could run on any CPU. Threads were dead because now we can go directly to the CPU Layer. For multiple CPU systems this was faster by a factor of CPU count (~9 times or more). The first time I experimented with this, the results were stunning. I never turned back. All prior designs were dead.

Then Reactive Extensions arrived. The idea was 'let's give the source the option of sending notifications when each row of data is ready' Instead of the caller deciding the when part, the source would now notify any registered observer. That pattern was clearly an enhanced event system which through the observer implemented a pseudo pub/sub architecture.

This all transpired around 2011. The architecture for definitive state control was set. The multi CPU design blew everything out of the water in performance.

Then Async/Await made Javascript promises simple and Reactive Extensions hit Javascript. Javascript observables were main stream.

Collapse
 
timsar2 profile image
timsar2 • Edited

It would be good if you make a best practice with StateApat in angular 16 and Signal.
Or any clue for me to upgrade and existing angular opensource project to version 16 with StateAdapt.

[github.com/fullstackhero/angular-m...]

Collapse
 
mfp22 profile image
Mike Pearson

All this investment people are doing in signals seems like overuse to me. I'm very optimistic about signals, but I have strict expectations for what I want to use for state management, and signals are inherently limited. Signals are not lazy. Signals are the new Angular change detection mechanism. It's best to think of them as the new AsyncPipe, at least for now.

The best practice for StateAdapt is easy to describe: Use RxJS for anything that might be shared, which is pretty much everything.

This is how I plan on using signals:

  • Using computed to derive component-specific state, from toSignal
  • Use the tight integration between new Angular APIs and signals, such as component inputs, to drive either 1. Derived state, like described above, or 2. Event sources for other state to update, using toObservable and toSource. Maybe I should create toSignalSource to combine those steps, since they will be common. Probably for Angular 17.

Over time, the role of signals will increase. Signals can be lazy, because they can be returned from functions. I wish I had time to work on it myself right now, but what I'm waiting for is TanStack Query for signals in Angular. That is a great API and example of lazily requesting shared async state.

Collapse
 
timsar2 profile image
timsar2

Thank you for clarifying the road to me.

Collapse
 
dwoodwardgb profile image
David Woodward

Sounds kind of intricate for most simple use cases.

Collapse
 
mfp22 profile image
Mike Pearson

Yeah, so are components though