DEV Community

loading...
Cover image for Jotai, now with Optics

Jotai, now with Optics

c0d3t3k profile image c0d3t3k Updated on ・5 min read

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!

Alt Text

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 />
...
Enter fullscreen mode Exit fullscreen mode

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"}
])
...
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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))))

Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

And that about does it!

Alt Text

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 and optics-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.

Discussion

pic
Editor guide