More and more functional paradigms have been finding their way into our contracting work. This really accelerated when we started using React Hooks a while back. In fact, back in the day, we were tasked to convert a legacy Angular ThreeJS project we had written earlier to React / react-three-fiber for performance, ease of maintenance, etc. Given the increasing complexity, we wanted a more atomic, composable state management system (of course this was before Recoil had been introduced). After some due diligence, we settled on Grammarly's Focal. This library, although a bit older, is powerful and introduced us to the intriguing FP concepts of Optics, Lenses, etc
Fast forward to now and we are learning more about Jotai, a Recoil alternative from Poimandres ( creators of react-three-fiber, etc.). Needless to say, we were very excited when we stumbled upon Jotai Issue #44, a discussion concerning focusable atoms started by Meris Bahtijaragic and the compelling work that resulted, jotai-optics. This code wraps another library we have been very intrigued by as of late, optics-ts which provides a whole new level of typesafe, functional goodness.
Now, if the concept of Optics is new to you, there are some excellent introductions in the context of functional programming. One such concise example is @gcanti's article on lenses and prisms, and there are plenty more. John DeGoes' Glossary of Functional Programming will also help with any new FP vocabulary. However, our humble goal here is to provide more of a practical (vs academic) example.
In order to explore this new functionality, we will use an existing Recoil example. We will not only convert to Jotai, but also add some extra functionality to soft introduce some benefits of jotai-optics
(and optics-ts
).
For this exercise, we thought it might be fun to upgrade Diogo Gancalves' cool Joeflix app to JotaiFlix!
Let's get started.
First, we we need to replace RecoilRoot
with the Jotai Provider
// App.js exceprt
...
//import {RecoilRoot} from 'recoil'
import { Provider } from "jotai";
...
function App() {
return (
/* <RecoilRoot> */
<Provider>
<JotaiDebugger />
<Router>
<FeedbackPopup />
...
Next, we will add some Favorites and History to the UI. This will give us some specific user generated state our Optics can act upon. In order to accomplish this, we need to first create some Jotai Atoms that will store this state. While we are at it, we will include some default values.
// state.js excerpt
...
export const historyAtom = atom([
{id: 62286, title: "Fear the Walking Dead", desc: "What did the world look like as it was transformin… the end of the world, will answer that question.", banner: "/58PON1OrnBiX6CqEHgeWKVwrCn6.jpg", type: "tv"},
{id: 528085, title: "2067", desc: undefined, banner: "/5UkzNSOK561c2QRy2Zr4AkADzLT.jpg", type: "movie"}
])
export const favoritesAtom = atom([
{id: 590223, title: "Love and Monsters", desc: undefined, banner: "/lA5fOBqTOQBQ1s9lEYYPmNXoYLi.jpg", type: "movie"},
{id: 76479, title: "The Boys", desc: "A group of vigilantes known informally as “The Boys” set out to take down corrupt superheroes with no more than blue-collar grit and a willingness to fight dirty.", banner: "/mGVrXeIjyecj6TKmwPVpHlscEmw.jpg", type: "tv"}
])
...
Now we need a function that determines if a given movie/show is already contained in either the Favorites or History collection. If it is present, it removes it, if not present it adds it.
Lets talk about what is happening here. In short, we use a jotai-optics wrapped optics-ts isomorphism to transform the internally passed atom collection passed by the outer focus
call.
Because we need to track both the current and converted boolean value, we create a wrapper object within the optic that has two properties (contained
and value
). The contained
property tracks the boolean output of the optic and the value
property tracks the array that potentially contains the specified item.
// optics.js
export const containsOptic = (item) => {
return O.optic()
.iso(
// Lens that is isomorphically converting an array given an item
// to a boolean determining whether the array contains that item.
(val) => ({
contained: (item && item.id) ? (_.findIndex(val, (currentItem) => item.id == currentItem.id) > -1) : false,
value: val
}),
(obj) => {
if(!(item && item.id)) {
return collection;
}
const collection = _.clone(obj.value);
const index = _.findIndex(collection, (currentItem) => item.id == currentItem.id);
if(obj.contained && index < 0) {
collection.push(item);
} else if(!obj.contained && index > -1) {
collection.splice(index, 1);
}
return collection;
}
)
.prop('contained');
To keep things relatively simple in the BigTile.js
, Tile.js
and Hero.js
files we call our containsOptic
factory function above to instantiate an optic that will provide not only History and Favorite state, but a way to easily set it.
// Tile.js excerpt
...
function Tile({data}) {
// https://github.com/merisbahti/jotai-optics
const [isInHistory, setIsInHistory] =
useAtom(focus(historyAtom, optic => optic.compose(containsOptic(data))))
const [isFavorite, setIsFavorite] =
useAtom(focus(favoritesAtom, optic => optic.compose(containsOptic(data))))
Finally, we'll add some icon buttons to call the respective setters created by the jotai-optics
focus
method above, to mutate the Favorites and History state.
// Continued Tile.js excerpt
const toggleFavorites = () => {
setIsFavorite(!isFavorite);
}
const playMedia = () => {
setIsInHistory(!isInHistory);
}
...
<button className="tile__play" onClick={() => toggleFavorites()}>
{isFavorite ? <AiFillHeart /> : <AiOutlineHeart />}
</button>
...
<button className="tile__play" onClick={playMedia}>
<img className="tile__icon" src={require('../images/streamline-icon-controls-play@15x15.png')} alt=""/>
</button>
...
And that about does it!
Final Thoughts:
- Using an optics based implementation ensures that state mutations can be modular and concise.
- With the @akeron's
optics-ts
library, powerful optics can be constructed, leading to easily repeatable patterns and clean architecture -
@merisbahti's
jotai-optics
provides a straightforward integration between Jotai andoptics-ts
. - Obviously, this was a very simple integration, but we feel it cracks open the door for some powerful functional programming integrations between Jotai and jotai-optics especially in light of the impressive feature set of optics-ts
Codesandbox example is included below.
NOTE: This sample code includes Jotai Dev Tools so be sure to use a Redux DevTools Browser Extension to easily observe the relevant state changes. For more info, please see our previous article.
Top comments (0)