Yet another solution for react state management based on profunctor algebra.
In profunctor optics a lens is like a focus within a data-structure that you can view or set, abstracting away the data-structure details. The key benefit of lens is composition. Lens compose easily. Another way to think about lens is just a pair of getter + setter.
const SomeComponent = props => {
const [model, setModel] = useState(initialModel);
}
Our profunctor (lens-ish) is a wrapper around the result of useState hook: [model:TModel, setModel: TModel -> unit] which obeys the profunctor algebraic structure.
The main advantage of representing the state as a profunctor is that you can easily move the focus on fields and items by promap-ing.
A value that implements the Profunctor specification must also implement the Functor specification.
p['fantasy-land/promap'](a => a, b => b)
is equivalent top
(identity)p['fantasy-land/promap'](a => f(g(a)), b => h(i(b)))
is equivalent top['fantasy-land/promap'](f, i)['fantasy-land/promap'](g, h)
(composition)
fantasy-land/promap :: Profunctor p => p b c ~> (a -> b, c -> d) -> p a d
const SomeComponent = props => {
const [modelLens] = useChangeTrackingState({a:1, b:2})
}
get is used to read the value from lens.
const SomeComponent = props => {
const lens = useChangeTrackingState(21)
const value = lens |> get // 21
}
set is used to set the value of a lens.
const SomeComponent = props => {
const lens = useChangeTrackingState(21)
(lens |> set)(22) // sets the model to 22
}
over - sets the state of a lens using some updater fn.
const SomeComponent = props => {
const lens = useChangeTrackingState('John')
over(lens)(x => x + ' Smith') // sets the model to 'John Smith'
}
Maps both the getter and the setter of a lens.
const SomeComponent = props => {
const lens = useChangeTrackingState({a:1, b:2})
const aLens = lens |> promap(R.prop('a'), R.assoc('a'))
console.log(aLens |> get) //1
(aLens |> set)(0) // sets the model to {a:0, b:2}
}
Maps only the getter of a lens.
const SomeComponent = props => {
const lens = useStateLens(null)
const lensOrDefault = lens |> lmap(x=> x || "default")
const value = lensOrDefault |> get //"default"
}
Maps only the setter of a lens.
const SomeComponent = props => {
const lens = useStateLens(1)
const lensOrDefault = lens |> rmap(x=> x || "default")
set(lensOrDefault)(null) //sets the model to "default"
}
Sequence transforms a lens of array into an array of lenses.
const SomeComponent = props => {
const lens = useStateLens([1,2,3])
const lenses = lens |> sequence
const firstItem = lenses[0] |> get //1
}
Pipes a lens to a Ramda lens. Both the getters and setters are piped.
const SomeComponent = props => {
const lens = useStateLens({a:1, b:2})
const aLens = pipe(lens, R.lens(R.prop('a'), R.assoc('a')))
console.log(aLens |> get) //1
(aLens |> set)(0) // sets the model to {a:0, b:2}
}
By using es6 proxy we were able to provide field and array indexer access just like with pojos.
const SomeComponent = props => {
const lens = useStateLens({name: 'John'})
const nameLens = lens.name
const name = nameLens |> get // John
}
const SomeComponent = props => {
const lens = useStateLens([1,2,3])
const firstItemLens = lens[0]
const firstItem = firstItemLens |> get // 1
}
const [personlens] = useStateLens({name:'John', age:23})
<...>
<TextField
value={personLens.name |> get}
onChange={personLens.name |> set |> onTextBoxChange}
/>