Getting Started
Optics is a library to manage global immutable state in TypeScript applications.
It let's you declare references to parts of your state, called optics, that allow you to read and update these parts as well as subscribe to their changes.
Optics are compositional in nature, making them a natural fit for component based UI frameworks.
Installation
bash
npm install @optics/react
bash
npm install @optics/react
(Requires React 18+)
Other frameworks
Adapters for different UI frameworks are planned as well.
In the meantime you can also use the framework agnostic version of the library:
bash
npm install @optics/state
bash
npm install @optics/state
This library has been built with TypeScript's type inference in mind, using it in strict mode is strongly recommended.
Usage
To create a global state call the createState
function and pass it an initial value:
ts
import {createState } from "@optics/react";constusersOptic =createState ([{name : "John",age : 42,address : {city : "New York",street : {name : "5th Avenue",number : 940 } },},]);
ts
import {createState } from "@optics/react";constusersOptic =createState ([{name : "John",age : 42,address : {city : "New York",street : {name : "5th Avenue",number : 940 } },},]);
It returns us an optic: a reference to a piece of application state that allows us to read, update this state as well as subscribe to it.
Read the state with get
:
ts
usersOptic .get (); // [{ name: "John", age: 42, address: { ... } }]
ts
usersOptic .get (); // [{ name: "John", age: 42, address: { ... } }]
Update it with set
:
ts
// adds a new userusersOptic .set ((prev ) => [...prev ,{name : "Jeanne",age : 32,address : {city : "Paris",street : {name : "Rue de Rivoli",number : 1 } },},]);usersOptic .get (); // [{ name: "John", age: 42, address: { ... } }, { name: "Jeanne", age: 32, address: { ... } }]
ts
// adds a new userusersOptic .set ((prev ) => [...prev ,{name : "Jeanne",age : 32,address : {city : "Paris",street : {name : "Rue de Rivoli",number : 1 } },},]);usersOptic .get (); // [{ name: "John", age: 42, address: { ... } }, { name: "Jeanne", age: 32, address: { ... } }]
Subscribe to it with subscribe
:
ts
usersOptic .subscribe ((users ) => {console .log ("users changed: ",users );});
ts
usersOptic .subscribe ((users ) => {console .log ("users changed: ",users );});
You're not restricted to a single global state, you can call createState
as many times as you want.
Deriving new optics
Where it gets interesting is that, from this base optic, you can get new ones focused on different parts of your state !
For exemple let's get an optic that focuses on the city of the first user in our list:
ts
constcityOptic =usersOptic [0].address .city ;
ts
constcityOptic =usersOptic [0].address .city ;
Now we can directly read and update the city of the first user with this optic:
ts
cityOptic .get (); // "New York"cityOptic .set ("Boston");cityOptic .get (); // "Boston"
ts
cityOptic .get (); // "New York"cityOptic .set ("Boston");cityOptic .get (); // "Boston"
Let's compare that with the manual way of updating immutable data:
ts
// 😵💫const newState = [{...users[0],address: {...users[0].address,city: "Boston",},},...users.slice(1),];
ts
// 😵💫const newState = [{...users[0],address: {...users[0].address,city: "Boston",},},...users.slice(1),];
Using optics saves us from quite the boilerplate when updating deeply nested data !
Optics let you focus on narrower parts of your state, allowing you to read and update these parts independently of the rest.
Another example:
ts
constjeanneStreetOptic =usersOptic [1].address .street ;jeanneStreetOptic .number .set (42);jeanneStreetOptic .get (); // { name: "Rue de Rivoli", number: 42 }
ts
constjeanneStreetOptic =usersOptic [1].address .street ;jeanneStreetOptic .number .set (42);jeanneStreetOptic .get (); // { name: "Rue de Rivoli", number: 42 }
Deriving a new optic looks just like accessing properties of an object !
You get the same type-safety and code completion in your editor as with plain javascript objects.
Usage in components
Your React components can subscribe to optics and re-render when the focused states change.
Call the useOptic
hook with an optic, it returns the current value inside a tuple.
tsx
import {useOptic } from "@optics/react";constStreetForm = () => {const [street ] =useOptic (jeanneStreetOptic );return (<div ><input value ={street .number }type ="number"onChange ={(e ) =>jeanneStreetOptic .number .set (parseInt (e .target .value ))}/>{street .name }</div >);};
tsx
import {useOptic } from "@optics/react";constStreetForm = () => {const [street ] =useOptic (jeanneStreetOptic );return (<div ><input value ={street .number }type ="number"onChange ={(e ) =>jeanneStreetOptic .number .set (parseInt (e .target .value ))}/>{street .name }</div >);};
The component will re-render when the street changes, whether it is changed from within the component or from elsewhere in the application.
ts
// outside of the componentjeanneStreetOptic .number .set ((prev ) =>prev + 1);// StreetForm re-renders 🔄
ts
// outside of the componentjeanneStreetOptic .number .set ((prev ) =>prev + 1);// StreetForm re-renders 🔄
However the component will not re-render if an unrelated part of the state changes.
ts
jeanneOptic .age .set ((prev ) =>prev + 1);// Jeanne's street reference hasn't changed, StreetForm doesn't re-render 🚫
ts
jeanneOptic .age .set ((prev ) =>prev + 1);// Jeanne's street reference hasn't changed, StreetForm doesn't re-render 🚫
Pass optics in props
Instead of referencing the optic directly, the component can accept one via its props.
tsx
import {useOptic ,Optic } from "@optics/react";interfaceProps {streetOptic :Optic <{name : string;number : number }>;}constStreetForm = ({streetOptic }:Props ) => {const [street ] =useOptic (streetOptic );// ...};
tsx
import {useOptic ,Optic } from "@optics/react";interfaceProps {streetOptic :Optic <{name : string;number : number }>;}constStreetForm = ({streetOptic }:Props ) => {const [street ] =useOptic (streetOptic );// ...};
Now StreetForm
isn't coupled to a specific part of the state anymore, it can take any optic that's focused on a street.
tsx
<>{/* Form for john's street */}<StreetForm streetOptic ={usersOptic [0].address .street } />{/* Form for Jeanne's street */}<StreetForm streetOptic ={usersOptic [1].address .street } /></>;
tsx
<>{/* Form for john's street */}<StreetForm streetOptic ={usersOptic [0].address .street } />{/* Form for Jeanne's street */}<StreetForm streetOptic ={usersOptic [1].address .street } /></>;
Next steps
Now that you know the basics of optics you can go through the core concepts to have a better grasp of the notions introduced here and learn about optic composition.