Skip to main content

useOptic

ts
function useOptic<T>(optic: Optic<T>): [T, ResultObject];
ts
function useOptic<T>(optic: Optic<T>): [T, ResultObject];

This hook allows your components to subscribe to an optic.
It returns a tuple with the value, and a set of options.

The component re-renders each time the focused value changes.

tsx
import { useOptic, createState } from "@optics/react";
 
const countOptic = createState(42);
 
const MyCount = () => {
const [count] = useOptic(countOptic);
 
// set new count value
countOptic.set(100);
 
return <div>count: {count}</div>;
};
tsx
import { useOptic, createState } from "@optics/react";
 
const countOptic = createState(42);
 
const MyCount = () => {
const [count] = useOptic(countOptic);
 
// set new count value
countOptic.set(100);
 
return <div>count: {count}</div>;
};

Options

The hook can take a set of options as a second argument.

- denormalize

boolean default: false

If set to true the value returned by useOptic is denormalized: the optics referenced in the value are replaced by their respective focused values.

tsx
const premiumPlanOptic = createState({
name: "Premium",
price: 100,
});
const userOptic = createState({
name: "John",
plan: premiumPlanOptic,
});
 
const [user] = useOptic(userOptic, { denormalize: true });
const user: { name: string; plan: { name: string; price: number; }; }
tsx
const premiumPlanOptic = createState({
name: "Premium",
price: 100,
});
const userOptic = createState({
name: "John",
plan: premiumPlanOptic,
});
 
const [user] = useOptic(userOptic, { denormalize: true });
const user: { name: string; plan: { name: string; price: number; }; }

Result

The hook returns a result object as second element of the tuple with the following properties:

tsx
const [, { setState, whenFocused, whenType, getOptics, getOpticsFromMapping }] =
useOptic(optic);
tsx
const [, { setState, whenFocused, whenType, getOptics, getOpticsFromMapping }] =
useOptic(optic);

- setState

Dispatch<SetStateAction<T>>

An update function for the optic with a stable reference.
Equivalent to: useCallback((value) => optic.set(value), [optic]).

- whenFocused

<R>(then: (totalOptic: Optic<T>) => R) => R | null

This function lets you narrow a partial optic (an optic that might be unfocused) to a total optic (an optic that's always focused, the default behavior).

The narrowed optic is provided as parameter of the callback, the latter only runs if the narrowing succeeds (if the optic is focused on a value).

tsx
const [, { whenFocused }] = useOptic(userPartialOptic);
const userPartialOptic: Optic<User, partial>
<>
{whenFocused((userTotalOptic) => (
<UserCard userOptic={userTotalOptic} />
(parameter) userTotalOptic: Optic<User, Omit<partial, "partial">>
))}
</>;
tsx
const [, { whenFocused }] = useOptic(userPartialOptic);
const userPartialOptic: Optic<User, partial>
<>
{whenFocused((userTotalOptic) => (
<UserCard userOptic={userTotalOptic} />
(parameter) userTotalOptic: Optic<User, Omit<partial, "partial">>
))}
</>;

It can also narrow the type of the focused value from nullable to non-nullable:

tsx
const [, { whenFocused }] = useOptic(nullableStringOptic);
const nullableStringOptic: Optic<string | undefined>
whenFocused((stringOptic) => {
(parameter) stringOptic: Optic<string, Omit<{}, "partial">>
});
tsx
const [, { whenFocused }] = useOptic(nullableStringOptic);
const nullableStringOptic: Optic<string | undefined>
whenFocused((stringOptic) => {
(parameter) stringOptic: Optic<string, Omit<{}, "partial">>
});

The function returns null if the optic is unfocused.

- whenType

<U extends T>(refine: (value: T) => U | false) => <R>(then: (narrowedOptic: Optic<U>) => R) => R | null

This function lets you narrow the type of the focused value in the optic.
It takes a first callback that narrows the value then returns it, and a second callback that only runs if the narrowing succeeds and that has the narrowed optic provided as parameter.

tsx
const [, { whenType }] = useOptic(stringOrNumberOptic);
const stringOrNumberOptic: Optic<string | number>
<>
{whenType((value) => typeof value === "number" && value)((numberOptic) => (
<NumericInput numberOptic={numberOptic} />
(parameter) numberOptic: Optic<number, Omit<{}, "partial">>
))}
{whenType((value) => typeof value === "string" && value)((stringOptic) => (
<Input stringOptic={stringOptic} />
(parameter) stringOptic: Optic<string, Omit<{}, "partial">>
))}
</>;
tsx
const [, { whenType }] = useOptic(stringOrNumberOptic);
const stringOrNumberOptic: Optic<string | number>
<>
{whenType((value) => typeof value === "number" && value)((numberOptic) => (
<NumericInput numberOptic={numberOptic} />
(parameter) numberOptic: Optic<number, Omit<{}, "partial">>
))}
{whenType((value) => typeof value === "string" && value)((stringOptic) => (
<Input stringOptic={stringOptic} />
(parameter) stringOptic: Optic<string, Omit<{}, "partial">>
))}
</>;

The function returns null if the narrowing fails.

It can also take an explicit type guard as first argument:

tsx
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
 
const [, { whenType }] = useOptic(petOptic);
const petOptic: Optic<Fish | Bird>
whenType(isFish)((fishOptic) => {
(parameter) fishOptic: Optic<Fish, Omit<{}, "partial">>
});
tsx
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
 
const [, { whenType }] = useOptic(petOptic);
const petOptic: Optic<Fish | Bird>
whenType(isFish)((fishOptic) => {
(parameter) fishOptic: Optic<Fish, Omit<{}, "partial">>
});

- getOptics

(getKey: (t: T[number]) => string) => readonly [key: string, optic: Optic<T[number]>][];

Only available if the optic is focused on an array, this function derives a new optic for each element of the array.

An element of the array will always have the same associated optic, even if the array is reordered, appended, prepended, ...
This is thanks to the getKey function that you provide, which returns a unique key for each element of the array.

tsx
const [, { getOptics }] = useOptic(usersOptic);
const usersOptic: Optic<User[]>
<>
{getOptics((user) => user.id).map(([key, userOptic]) => (
<User key={key} userOptic={userOptic} />
(parameter) userOptic: Optic<User, Omit<{}, "partial">>
))}
</>;
tsx
const [, { getOptics }] = useOptic(usersOptic);
const usersOptic: Optic<User[]>
<>
{getOptics((user) => user.id).map(([key, userOptic]) => (
<User key={key} userOptic={userOptic} />
(parameter) userOptic: Optic<User, Omit<{}, "partial">>
))}
</>;

Since keys serve the same purpose as in React, you can reuse the generated key and pass it to the key prop of the component.

- getOpticsFromMapping

(getKey: (t: T) => string) => readonly [key: string, optic: Optic<T>][];

This function is similar to getOptics, but it derive the optics from the values of a mapped optics instead of an array.

tsx
const [, { getOpticsFromMapping }] = useOptic(userMappedOptic);
const userMappedOptic: Optic<User, mapped>
<>
{getOpticsFromMapping((user) => user.id).map(([key, userOptic]) => (
<User key={key} userOptic={userOptic} />
(parameter) userOptic: Optic<User, Omit<mapped, "map" | "partial">>
))}
</>;
tsx
const [, { getOpticsFromMapping }] = useOptic(userMappedOptic);
const userMappedOptic: Optic<User, mapped>
<>
{getOpticsFromMapping((user) => user.id).map(([key, userOptic]) => (
<User key={key} userOptic={userOptic} />
(parameter) userOptic: Optic<User, Omit<mapped, "map" | "partial">>
))}
</>;