### Install Saphyra Package Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Install the Saphyra package using your preferred package manager. Supported managers include npm, yarn, pnpm, and bun. ```bash npm install saphyra ``` ```bash yarn add saphyra ``` ```bash pnpm add saphyra ``` ```bash bun add saphyra ``` -------------------------------- ### Transition Naming Examples Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/transitions.mdx Provides practical examples of transition naming conventions, including hierarchical structures for entities and actions, simple operations, and single resource operations. ```javascript ["board", board.id, "column", column.id, "todo", todo.id, "toggle"] ``` ```javascript ["user", userId, "profile", "update"] ``` ```javascript ["project", projectId, "task", taskId, "assign"] ``` ```javascript ["auth", "role"] ``` ```javascript ["pokemon"] ``` -------------------------------- ### Create Saphyra Store Definition Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Define a new store using the `newStoreDef` function from the 'saphyra' library. This function returns a store factory that can be used for instantiation. ```javascript import { newStoreDef } from "saphyra" const newCountStore = newStoreDef() ``` -------------------------------- ### Transition Naming Convention Example Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/transitions.mdx Illustrates the hierarchical naming pattern for transitions, emphasizing containment and specificity. This convention helps in organizing actions and deriving loading states. ```javascript ["board", board.id, "column", column.id, "todo", todo.id, "toggle"] ``` -------------------------------- ### Async Effects in Store Initialization with Saphyra JS Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx This example demonstrates how to leverage asynchronous effects during the initialization of a Saphyra store. It shows an `onConstruct` function that fetches data asynchronously based on initial properties, storing the fetched data as a derived state. The `async` and `promise` functions within the reducer are used to handle the asynchronous operation. Dependencies include `newStoreDef`, `fetchProducts`, and asynchronous handling utilities. ```javascript const newProductsStore = newStoreDef({ async onConstruct({ initialProps }) { const { category } = initialProps // we're storing the category as the source this time return { category } // [!code ++] }, reducer({ state, diff, set, async }) { diff() .on([s => s.category]) .run(category => { async().promise(async ({ signal }) => { const products = await fetchProducts(category, signal) // [!code ++] set({ $products: products }) // [!code ++] }) }) return state }, }) const productsStore = newProductsStore({ category: "smartphones" }) await productsStore.waitFor(["bootstrap"]) console.log(productsStore.getState()) // > { $products: [{...}, {...}], category: "smartphones" } ``` -------------------------------- ### React Example: Managing Pending States with Saphyra Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/guides/pending-states.mdx This example demonstrates how to use Saphyra's `useTransition` hook in a React component to manage and display pending states for asynchronous operations like liking a post. It shows how to dispatch actions with transitions and conditionally render UI elements based on the pending state. ```tsx const newSocialMediaStore = newStoreDef({ reducer({ state, set, diff, async, deps, action }) { if (action.type === "like-post") { async() .onFinish(revalidate()) .promise(async ({ signal }) => { await deps.likePost(action.postId, signal) }) } return state }, }) const SocialMedia = createStoreUtils() function Post({ post }) { const [socialMedia] = SocialMedia.useStore() const isPending = SocialMedia.useTransition(["post", post.id, "like"]) return ( ) } ``` -------------------------------- ### Provide Saphyra Store with React Context Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Provide Saphyra store state to nested components using React Context. The `createStoreUtils` function generates a `Context` component and a `useStore` hook for seamless consumption. ```javascript import { newStoreDef } from "saphyra" import { useNewStore, createStoreUtils } from "saphyra/react" const newCountStore = newStoreDef() const CountStore = createStoreUtils() function CountProvider({ initialCount, children }) { const [countStore, resetStore, isLoading] = useNewStore( () => newCountStore({ count: initialCount }) ) return ( // [!code ++] {children} // [!code ++] ) } function Children() { const [countStore] = CountStore.useStore() const count = CountStore.useSelector(s => s.count) const setToTen = () => countStore.setState({ count: 10 }) return (
{count}
) } function App() { return ( ) } ``` -------------------------------- ### Instantiate Saphyra Store with React Hook Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Instantiate a Saphyra store within a React component using the `useNewStore` hook. This hook takes a factory function that returns the store instance and provides the store, a reset function, and a loading state. ```javascript import { newStoreDef } from "saphyra" import { useNewStore } from "saphyra/react" const newCountStore = newStoreDef() function App() { const [countStore, resetStore, isLoading] = useNewStore( () => newCountStore({}) ) } ``` -------------------------------- ### Revalidation Strategies: dispatchAsync vs Manual Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Compares two methods for triggering data revalidation after a subtransition: using `dispatchAsync` and manually calling revalidation functions, providing an example of `dispatchAsync` within `onFinish`. ```APIDOC ## Revalidation with `dispatchAsync` ### Description This example shows how to use `dispatchAsync` within an `onFinish` callback to trigger asynchronous revalidation. It includes logic to abort previous revalidation attempts if they are still in progress when a new one is initiated. ### Example: `dispatchAsync` in `onFinish` ```javascript async() .setName("toggle-todo") .onFinish({ id: ["revalidate-todos"], fn: (resolve, reject, { isLast }) => { if (!isLast()) return; dispatchAsync({ type: "fetch-todos", transition: ["revalidate-todos"], beforeDispatch: ({ action, store, transition }) => { if (!isLast()) return; store.abort(transition); return action; }, }, { onAbort: "noop" }) .then(resolve) .catch(reject); // Cleanup function to abort the revalidation if a new one starts return () => { store.abort(["revalidate-todos"]); }; }, }) .promise(async ctx => { await toggleTodoInDb(action.todoId, ctx.signal); }); ``` ``` -------------------------------- ### Example: Simple Counter Derivations (TypeScript) Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/cached-getters.mdx Provides a concise example of defining state for a counter and creating cached getters for derived values like doubled and squared counts. ```typescript type State = { count: number getDoubledCount: () => number getSquaredCount: () => number } const CounterStore = newStoreDef({ derivations: { getDoubledCount: { selectors: [s => s.count], evaluator: count => count * 2, }, getSquaredCount: { selectors: [s => s.count], evaluator: count => count * count, }, }, }) ``` -------------------------------- ### Read Saphyra Store State in React Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Read Saphyra store state within React components using `createStoreUtils` and `useSelector`. This utility generates hooks to translate store state into React state, enabling efficient updates. ```javascript import { newStoreDef } from "saphyra" import { useNewStore, createStoreUtils } from "saphyra/react" const newCountStore = newStoreDef() const CountStore = createStoreUtils() function App() { const [countStore, resetStore, isLoading] = useNewStore( () => newCountStore({ count: 0 }) ) const count = CountStore.useSelector(s => s.count, countStore) // [!code ++] return {count} // [!code ++] } ``` -------------------------------- ### Handle Optimistic UI Updates with Saphyra Source: https://context7.com/vitormarkis/saphyra/llms.txt This snippet illustrates how to implement optimistic UI updates using Saphyra. It allows for immediate visual feedback on user actions, with automatic rollback in case of errors during asynchronous operations. The example defines a post store with a 'toggle-like' action and uses `runOptimisticUpdateOn`, `optimistic`, `async`, and `onError` for managing the state transitions. The core dependency is the 'saphyra' library. ```typescript import { newStoreDef } from 'saphyra' type PostState = { likes: number isLiked: boolean error: string | null } type PostActions = { type: 'toggle-like' } async function toggleLikeAPI( postId: string, isLiked: boolean, signal: AbortSignal ): Promise { await new Promise(resolve => setTimeout(resolve, 800)) if (signal.aborted) throw new Error('Aborted') // Simulate occasional failure if (Math.random() < 0.2) throw new Error('Network error') } const createPostStore = newStoreDef({ config: { // Configure which actions should run optimistic updates runOptimisticUpdateOn({ action }) { return action.type === 'toggle-like' } }, onConstruct({ initialProps }) { return { likes: initialProps.likes || 0, isLiked: false, error: null, } }, reducer({ state, action, async, optimistic, set }) { if (action.type === 'toggle-like') { const newIsLiked = !state.isLiked const likeDelta = newIsLiked ? 1 : -1 // Apply optimistic update immediately optimistic({ isLiked: newIsLiked, likes: state.likes + likeDelta, }) // Make async request async() .setName('toggle-like-request') .promise(async ({ signal }) => { await toggleLikeAPI('post-123', newIsLiked, signal) // Commit the optimistic changes set({ isLiked: newIsLiked, likes: state.likes + likeDelta, error: null, }) }) .onError(error => { // On error, optimistic update is automatically rolled back // Just need to set error message set({ error: error.message }) }) } return state } }) // Usage const postStore = createPostStore({ likes: 42 }) // Subscribe to optimistic state for immediate UI updates postStore.subscribeOptimistic(state => { console.log('Optimistic state:', state.likes, state.isLiked) }) // Subscribe to committed state for final values postStore.subscribe(state => { console.log('Committed state:', state.likes, state.isLiked) }) // Toggle like postStore.dispatch({ type: 'toggle-like', transition: ['like'] }) // Immediately shows optimistic update // After API resolves, either commits or rolls back ``` -------------------------------- ### Optimistic State: Toggle Todo Example Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/optimistic.mdx Demonstrates using optimistic state to immediately update the UI when toggling a todo item. It predicts the updated todo list and applies it optimistically before the asynchronous database operation completes. If the operation fails, the state reverts. ```typescript reducer({ action, async, set, state, optimistic }) { async().promise(async ({ signal }) => { optimistic({ todos: state.todos.map(todo => todo.id === action.todoId ? { ...todo, completed: !todo.completed } : todo ), }) await toggleTodoDb(action.todoId, signal) const todos = await fetchTodosDb(signal) set({ todos }) }) } ``` -------------------------------- ### Async Derived Values with Saphyra Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/guides/async-derived-values.mdx This example shows how to define an asynchronous derived value `$movies` in Saphyra. When the `query` state changes, the `diff` function triggers an asynchronous operation to fetch movies using `deps.fetchMovies`. The fetched movies are then set to the `$movies` state. This pattern is useful for managing data that depends on other state variables and requires asynchronous fetching. ```typescript type State = { query: string $movies: Movie[] } const store = newStoreDef({ reducer({ state, set, diff, async, deps }) { diff() .on([s => s.query]) .run(query => { async().promise(async ({ signal }) => { const movies = await deps.fetchMovies(query, signal) set(s => ({ $movies: movies })) }) }) return state }, }) ``` -------------------------------- ### Fetch Initial Data in onConstruct - JavaScript Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx Demonstrates an asynchronous `onConstruct` function that fetches data based on initial properties. It uses a `fetchProducts` helper function and returns the fetched products as part of the initial state. ```javascript const newProductsStore = newStoreDef({ async onConstruct({ initialProps }) { const { category } = initialProps const products = await fetchProducts(category) return { products } }, }) async function fetchProducts(category) { const response = await fetch(`https://dummyjson.com/products/category/${category}`) const products = await response.json() return products } ``` -------------------------------- ### Write Saphyra Store State in React Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/getting-started.mdx Update Saphyra store state from React components using the `setState` method on the store instance. This is suitable for non-critical updates like UI state changes. ```javascript import { newStoreDef } from "saphyra" import { useNewStore, createStoreUtils } from "saphyra/react" const newCountStore = newStoreDef() const CountStore = createStoreUtils() function App() { const [countStore, resetStore, isLoading] = useNewStore( () => newCountStore({ count: 0 }) ) const count = CountStore.useSelector(s => s.count, countStore) return (
{count}
) } ``` -------------------------------- ### Deriving Loading States with useTransition Hook Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/transitions.mdx Demonstrates how to use the `useTransition` hook with hierarchical transition names to derive granular loading states for various operations, from broad categories to specific actions. ```javascript // All board operations const isBoardLoading = useTransition(["board"]) // Specific board operations const isSpecificBoardLoading = useTransition(["board", boardId]) // All todos in a specific column const isColumnTodosLoading = useTransition([ "board", boardId, "column", columnId, "todo", ]) // Specific todo toggle action const isTodoToggleLoading = useTransition([ "board", boardId, "column", columnId, "todo", "toggle", ]) ``` -------------------------------- ### Managing Async Operations with onFinish Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Demonstrates how to use `resolve` and `reject` within the `onFinish` callback to control the asynchronous operation's outcome and how to implement cleanup functions. ```APIDOC ## onFinish Callback Lifecycle ### Description The `fn` callback within `onFinish` can optionally return a cleanup function. This function is executed when a new `fn` with the same `id` is invoked, allowing for resource management or cancellation of previous operations. ### Example: Using resolve and reject ```javascript onFinish({ id: ["user"], fn: (resolve, reject) => { setTimeout(() => { const wentGood = Math.random() > 0.5; wentGood ? resolve() : reject(); }, 1000); } }); ``` ### Example: Implementing a Cleanup Function ```javascript onFinish({ id: ["user"], fn: (resolve, reject) => { const id = setTimeout(() => { const wentGood = Math.random() > 0.5; wentGood ? resolve() : reject(); }, 1000); // Returns a cleanup function return () => { resolve(); // Ensure the transition settles if not already clearTimeout(id); }; } }); ``` ``` -------------------------------- ### Optimistic Update Implementation with Transitions Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/transitions.mdx Explains the concept of optimistic updates within Saphyra transitions. It highlights how registering optimistic setters provides immediate UI feedback and how the transition automatically handles rollbacks if the operation fails. ```javascript // In a transition context: // register an optimistic setter optimisticSetter(state => { state.items.push(newItem) }) // perform async operation... // if operation fails, the optimistic setter is automatically discarded. ``` -------------------------------- ### Recreate Store Instance on Entity Change - JavaScript Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx Illustrates recreating a store instance when the core entity it represents changes, such as when a `todoId` prop changes. This is managed using `useEffect` to reset the store with new initial props. ```javascript const newTodoStore = newStoreDef({ onConstruct: ({ initialProps: { todoId } }) => fetchTodo(todoId), }) const Todo = createStoreUtils() function TodoView({ todoId }) { const [todoStore, resetStore, isLoading] = useNewStore( () => newTodoStore({ todoId }) ) useEffect(() => { resetStore(newTodoStore({ todoId })) }, [todoId]) // ... ``` -------------------------------- ### Initialize Store State from Props - JavaScript Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx Defines an `onConstruct` function to process initial properties and return the initial state of the store. It splits a 'name' prop into 'firstName' and 'lastName'. ```javascript const newAuthStore = newStoreDef({ onConstruct({ initialProps }) { const { name } = initialProps const [firstName, lastName] = name.split(" ") return { firstName, lastName } }, }) const authStore = newAuthStore({ name: "John Doe" }) console.log(authStore.getState()) // > { firstName: "John", lastName: "Doe" } ``` -------------------------------- ### onFinish with Explicit Resolve/Reject Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Illustrates how to control the lifecycle of an onFinish callback by explicitly calling `resolve` or `reject`. This is crucial for managing asynchronous tasks that determine the success or failure of the transition. ```javascript onFinish({ id: ["user"], fn: (resolve, reject) => { setTimeout(() => { const wentGood = Math.random() > 0.5 wentGood ? resolve() : reject() }, 1000) }, }) ``` -------------------------------- ### Optimistic State: Increment Counter Example Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/optimistic.mdx Illustrates optimistic state for a simple counter increment. It predicts the incremented count and applies it immediately. The asynchronous function then updates the actual count, and if it fails, the optimistic update is discarded. ```typescript reducer({ set, state, async, optimistic }) { optimistic({ count: state.count + 1 }) async().promise(async () => { const newCounter = await incrementCounter(state.count) set({ count: newCounter }) }) return state } ``` -------------------------------- ### Manual Error Handling in onFinish Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Illustrates manual error handling within an `onFinish` callback. When `getTodosFromDb` fails, the error is manually emitted to Saphyra's error system using `store.emitError` before rejecting the promise. ```javascript onFinish({ id: ["todo"], fn: (resolve, reject) => { getTodosFromDb(controller.signal) .then(todos => { set({ todos }) resolve() }) .catch(error => { store.emitError(error) // [!code ++] reject(error) }) } }) ``` -------------------------------- ### Define Memoized Derived State with Saphyra (TypeScript) Source: https://context7.com/vitormarkis/saphyra/llms.txt This snippet shows how to define memoized derived state properties using Saphyra's `newStoreDef`. The `derivations` configuration allows specifying getters that depend on other state properties (`on`). These getters are recomputed only when their dependencies change, improving performance. It includes examples for calculating a full name, total spent from orders, and the count of pending orders. ```typescript import { newStoreDef } from 'saphyra' type UserState = { firstName: string lastName: string email: string orders: Array<{ id: string; total: number; status: string }> // Cached getters getFullName: () => string getTotalSpent: () => number getPendingOrdersCount: () => number } const createUserStore = newStoreDef({ // Define derivations configuration derivations: d => ({ getFullName: d() .on([s => s.firstName, s => s.lastName]) .evaluate((firstName, lastName) => { console.log('Computing full name...') return `${firstName} ${lastName}` }), getTotalSpent: d() .on([s => s.orders]) .evaluate(orders => { console.log('Computing total spent...') return orders.reduce((sum, order) => sum + order.total, 0) }), getPendingOrdersCount: d() .on([s => s.orders]) .evaluate(orders => { console.log('Computing pending orders...') return orders.filter(o => o.status === 'pending').length }), }), onConstruct({ initialProps }) { return { firstName: initialProps.firstName, lastName: initialProps.lastName, email: initialProps.email, orders: initialProps.orders || [], } }, reducer({ state }) { return state } }) // Usage const userStore = createUserStore({ firstName: 'John', lastName: 'Doe', email: 'john@example.com', orders: [ { id: '1', total: 99.99, status: 'completed' }, { id: '2', total: 49.99, status: 'pending' }, { id: '3', total: 149.99, status: 'completed' }, ] }) const state = userStore.getState() // First call computes the value console.log(state.getFullName()) // Logs: "Computing full name..." then "John Doe" // Subsequent calls return cached value (no recomputation) console.log(state.getFullName()) // Just returns "John Doe", no log // Only recomputes when dependencies change userStore.setState({ firstName: 'Jane' }) const newState = userStore.getState() console.log(newState.getFullName()) // Logs: "Computing full name..." then "Jane Doe" console.log(state.getTotalSpent()) // Logs: "Computing total spent..." then 299.97 console.log(state.getPendingOrdersCount()) // Logs: "Computing pending orders..." then 1 ``` -------------------------------- ### Manual Revalidation with AbortController Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Demonstrates manual revalidation using an `AbortController` to manage the lifecycle of the asynchronous `getTodosFromDb` operation. The cleanup function aborts the controller. ```javascript onFinish({ id: ["todo"], fn: (resolve, reject) => { const controller = new AbortController() getTodosFromDb(controller.signal) .then(todos => { if (controller.signal.aborted) { return resolve() } set({ todos }) resolve() }) .catch(reject) return () => controller.abort() } }) ``` -------------------------------- ### Basic onFinish Usage with Async Operation Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Demonstrates the fundamental usage of the onFinish API to execute an asynchronous operation after a subtransition. The `id` property is used for referencing, and the `fn` contains the asynchronous logic. ```javascript async() .setName("update-profile") .onFinish({ id: ["user"], fn: () => { console.log("Some user related async operation has finished!") }, }) .promise(async ctx => { await deps.updateUserProfile(action.payload, ctx.signal) }) ``` -------------------------------- ### Manual Todo Revalidation with Promise Call Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Demonstrates manual revalidation of todos by directly calling a promise-based function (`getTodosFromDb`) within an `onFinish` callback. This approach requires manual handling of state updates and error propagation. ```javascript async() .setName("toggle-todo") .onFinish({ id: ["revalidate-todos"], fn: (resolve, reject, { isLast }) => { if (!isLast()) return getTodosFromDb() .then(todos => { set({ todos }) resolve() }) .catch(reject) }, }) .promise(async ctx => { await toggleTodoInDb(action.todoId, ctx.signal) }) ``` -------------------------------- ### onFinish Cleanup Function Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Shows how to return a cleanup function from the `fn` callback in onFinish. This function executes when a new `fn` with the same `id` is called, allowing for resource management and preventing potential issues like hanging transitions. ```javascript onFinish({ id: ["user"], fn: (resolve, reject) => { const id = setTimeout(() => { const wentGood = Math.random() > 0.5 wentGood ? resolve() : reject() }, 1000) // ✅ Subtransition settle return () => { resolve() clearTimeout(id) } }, }) ``` -------------------------------- ### Manage Async Operations with Saphyra Transitions (TypeScript) Source: https://context7.com/vitormarkis/saphyra/llms.txt This snippet demonstrates how to handle asynchronous operations like fetching and adding todos using Saphyra's `newStoreDef` and the `async()` function for managing transitions. It covers defining state, actions, simulating API calls, and dispatching actions with transition names for tracking loading states and errors. ```typescript import { newStoreDef } from 'saphyra' type TodoState = { items: string[] error: string | null } type TodoActions = | { type: 'fetch-todos' } | { type: 'add-todo'; text: string } // Simulate API call async function fetchTodosFromAPI(signal: AbortSignal): Promise { await new Promise(resolve => setTimeout(resolve, 1000)) if (signal.aborted) throw new Error('Aborted') return ['Buy milk', 'Walk dog', 'Write code'] } async function addTodoToAPI(text: string, signal: AbortSignal): Promise { await new Promise(resolve => setTimeout(resolve, 500)) if (signal.aborted) throw new Error('Aborted') } const createTodoStore = newStoreDef({ onConstruct({ initialProps }) { return { items: [], error: null, } }, reducer({ state, action, async, set, events }) { if (action.type === 'fetch-todos') { // async() wraps promise and manages transition lifecycle async() .setName('fetch-todos-request') .promise(async ({ signal }) => { const todos = await fetchTodosFromAPI(signal) set({ items: todos, error: null }) }) .onError(error => { set({ error: error.message }) }) } if (action.type === 'add-todo') { const newTodo = action.text async() .setName('add-todo-request') .promise(async ({ signal }) => { await addTodoToAPI(newTodo, signal) set(s => ({ items: [...s.items, newTodo] })) events.emit('todo-added', newTodo) }) .onError(error => { set({ error: error.message }) }) } return state } }) // Usage const todoStore = createTodoStore({}) // Dispatch with transition name for tracking todoStore.dispatch({ type: 'fetch-todos', transition: ['fetch', 'todos'] }) // Check if transition is in progress const isLoading = todoStore.transitions.isHappening(['fetch', 'todos']) console.log('Loading:', isLoading) // true // Wait for transition to complete await todoStore.waitFor(['fetch', 'todos']) console.log('Todos:', todoStore.state.items) // ['Buy milk', 'Walk dog', 'Write code'] ``` -------------------------------- ### onFinish with dispatchAsync for Revalidation Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Illustrates using onFinish in conjunction with `dispatchAsync` for data revalidation after a subtransition. It includes logic to conditionally abort transitions and handle revalidation processes, ensuring data freshness. ```javascript async() .setName("toggle-todo") .onFinish({ id: ["revalidate-todos"], fn: (resolve, reject, { isLast }) => { if (!isLast()) return dispatchAsync({ type: "fetch-todos", transition: ["revalidate-todos"], beforeDispatch: ({ action, store, transition }) => { if (!isLast()) return store.abort(transition) return action }, }, { onAbort: "noop" }) .then(resolve) .catch(reject) return () => { store.abort(["revalidate-todos"]) } }, }) .promise(async ctx => { await toggleTodoInDb(action.todoId, ctx.signal) }) ``` -------------------------------- ### Cleanup Side Effects with Abort Signal - JavaScript Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx Shows how to use an abort signal within `onConstruct` to clean up side effects, such as closing WebSockets or dismissing toasts, when the store is disposed. ```javascript const newTodosStore = newStoreDef({ async onConstruct({ initialProps, signal, deps }) { const todos = await fetchTodos({ signal }) const websocket = deps.createWebsocket("ws://localhost:3000") signal.addEventListener("abort", websocket.close) const cleanToast = deps.fireToast() signal.addEventListener("abort", cleanToast) return { todos, websocket } }, }) ``` -------------------------------- ### Using dispatchAsync for Conflict Resolution Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Illustrates how `dispatchAsync` with a `beforeDispatch` hook can resolve conflicts by canceling previous operations. This is a core feature inherited from Saphyra's `dispatch` for managing concurrent async tasks. ```javascript dispatchAsync({ type: "fetch-todos", transition: ["revalidate-todos"], beforeDispatch({ action, store, transition }) { if (store.transitions.isHappeningUnique(transition)) { store.abort(transition) // Cancel previous operation } return action }, }) ``` -------------------------------- ### onFinish API Usage Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx The onFinish API lets you run a callback once a subtransition finishes. It's designed for asynchronous operations and allows you to control their lifecycle using an ID. ```APIDOC ## POST /api/onFinish ### Description Declares a callback function to be executed after a subtransition completes. This function is asynchronous by default and can pause the transition until `resolve` or `reject` is called. An `id` can be provided to reference and manage the asynchronous operation's lifecycle. ### Method POST ### Endpoint `/api/onFinish` ### Parameters #### Request Body - **id** (string[]) - Required - A unique identifier for the asynchronous operation. - **fn** (function) - Required - The callback function to execute. It receives `resolve`, `reject`, and an optional context object as arguments. ### Request Example ```json { "id": ["user"], "fn": "() => { console.log('Operation finished!'); }" } ``` ### Response #### Success Response (200) - **status** (string) - Indicates the success of the operation. #### Response Example ```json { "status": "success" } ``` ``` -------------------------------- ### Create Store Definition with newStoreDef (TypeScript) Source: https://context7.com/vitormarkis/saphyra/llms.txt Defines a reusable store factory using `newStoreDef`, including initialization logic, state shape with derived properties, reducer functions for state updates, and action types. This allows for multiple store instantiations with different initial states. The `diff` function enables efficient derivation updates only when specified state dependencies change. ```typescript import { newStoreDef } from 'saphyra' // Define state shape with derived properties ($ prefix convention) type CounterState = { count: number $doubled: number $squared: number } // Define action types type CounterActions = | { type: 'increment' } | { type: 'decrement' } | { type: 'reset' } // Create store definition const createCounter = newStoreDef({ // Initialize state from props onConstruct({ initialProps }) { return { count: initialProps.count || 0, $doubled: 0, $squared: 0, } }, // Define reducer logic reducer({ state, action, diff, set }) { if (action.type === 'increment') { set(s => ({ count: s.count + 1 })) } if (action.type === 'decrement') { set(s => ({ count: s.count - 1 })) } if (action.type === 'reset') { set({ count: 0 }) } // Derive values when dependencies change if (diff().on([s => s.count]).check()) { set(s => ({ $doubled: s.count * 2, $squared: s.count * s.count })) } return state } }) // Instantiate the store const counter = createCounter({ count: 5 }) // Use the store counter.dispatch({ type: 'increment', transition: ['increment'] }) console.log(counter.state.count) // 6 console.log(counter.state.$doubled) // 12 ``` -------------------------------- ### dispatchAsync Revalidation in onFinish Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Shows how to trigger a revalidation using `dispatchAsync` within an `onFinish` callback. It also includes a cleanup function that aborts the revalidation transition if the operation is interrupted. ```javascript onFinish({ id: ["todo"], fn: (resolve, reject) => { dispatchAsync({ type: "revalidate", transition: ["revalidate-todos"], }) .then(resolve) .catch(reject) return () => store.abort(["revalidate-todos"]) } }) ``` -------------------------------- ### Derive Values in `onConstruct` with Saphyra JS Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/on-construct.mdx This snippet illustrates how to derive new values from initial properties within the `onConstruct` function of a Saphyra store. It uses the `useNewStore` hook and defines a reducer to split a name into first and last names, setting these derived values in the store's state. Dependencies include the `useNewStore` hook and `newStoreDef`. ```javascript const [store] = useNewStore(() => newNameStore(user)) const newNameStore = newStoreDef({ onConstruct({ initialProps }) { const { user } = initialProps return { name: user.name } // just the source // [!code ++] }, reducer({ state, diff, set }) { diff() .on([s => s.name]) .run(name => { const [$firstName, $lastName] = name.split(" ") set({ $firstName, $lastName }) // derived values // [!code ++] }) return state // final state // [!code ++] }, }) ``` -------------------------------- ### Saphyra: Emit and Listen to Custom Auth Events (TypeScript) Source: https://context7.com/vitormarkis/saphyra/llms.txt This TypeScript code defines a Saphyra store for authentication, including state, actions, and custom events. It demonstrates how to emit 'user-logged-in', 'user-logged-out', and 'auth-error' events and how to listen to them for side effects like local storage manipulation and console logging. The `loginAPI` function simulates an asynchronous login process. ```typescript import { newStoreDef, EventEmitter } from 'saphyra' type AuthState = { user: { id: string; name: string } | null token: string | null } type AuthActions = | { type: 'login'; email: string; password: string } | { type: 'logout' } // Define custom events type AuthEvents = { 'user-logged-in': [user: { id: string; name: string }, token: string] 'user-logged-out': [] 'auth-error': [error: Error] } async function loginAPI( email: string, password: string, signal: AbortSignal ): Promise<{ user: { id: string; name: string }; token: string }> { await new Promise(resolve => setTimeout(resolve, 1000)) if (signal.aborted) throw new Error('Aborted') return { user: { id: '123', name: 'John Doe' }, token: 'jwt-token-here' } } const createAuthStore = newStoreDef< AuthState, AuthState, AuthActions, AuthEvents >({ onConstruct() { return { user: null, token: null, } }, reducer({ state, action, async, set, events }) { if (action.type === 'login') { async() .promise(async ({ signal }) => { const result = await loginAPI( action.email, action.password, signal ) set({ user: result.user, token: result.token }) // Emit event after successful login events.emit('user-logged-in', result.user, result.token) }) .onError(error => { events.emit('auth-error', error) }) } if (action.type === 'logout') { set({ user: null, token: null }) events.emit('user-logged-out') } return state } }) // Usage const authStore = createAuthStore({}) // Listen to events authStore.events.on('user-logged-in', (user, token) => { console.log('User logged in:', user.name) // Store token in localStorage localStorage.setItem('auth-token', token) // Send analytics event // analytics.track('User Logged In', { userId: user.id }) }) authStore.events.on('user-logged-out', () => { console.log('User logged out') // Clear localStorage localStorage.removeItem('auth-token') // Redirect to login page // router.push('/login') }) authStore.events.on('auth-error', error => { console.error('Auth error:', error.message) // Show toast notification // toast.error(error.message) }) // Trigger login authStore.dispatch({ type: 'login', email: 'user@example.com', password: 'password123', transition: ['auth', 'login'] }) // Events automatically fire when async operation completes ``` -------------------------------- ### Provide Scoped Stores with React Context using Saphyra Source: https://context7.com/vitormarkis/saphyra/llms.txt This snippet demonstrates how to provide Saphyra stores through React Context, allowing for scoped store instances within component subtrees. It showcases creating a tab store, providing it via a Context.Provider, and consuming it in child components without prop drilling. Dependencies include 'saphyra/react' and 'saphyra'. ```typescript import { createStoreUtils, useNewStore } from 'saphyra/react' import { newStoreDef } from 'saphyra' import { useState as useReactState } from 'react' type TabsState = { activeTab: string tabs: Array<{ id: string; label: string }> } type TabsActions = { type: 'select-tab'; id: string } const createTabsStore = newStoreDef({ onConstruct({ initialProps }) { return initialProps }, reducer({ state, action, set }) { if (action.type === 'select-tab') { set({ activeTab: action.id }) } return state } }) const Tabs = createStoreUtils() // Parent component provides store via context function TabsContainer({ children }: { children: React.ReactNode }) { const [tabsStore, resetStore, isLoading] = useNewStore(() => createTabsStore({ activeTab: 'home', tabs: [ { id: 'home', label: 'Home' }, { id: 'profile', label: 'Profile' }, { id: 'settings', label: 'Settings' }, ] }) ) return ( {children} ) } // Child components consume from context (no store prop needed) function TabsList() { const [tabsStore] = Tabs.useStore() const tabs = Tabs.useCommittedSelector(s => s.tabs) const activeTab = Tabs.useCommittedSelector(s => s.activeTab) return (
{tabs.map(tab => ( ))}
) } function TabsContent() { const activeTab = Tabs.useCommittedSelector(s => s.activeTab) return (
{activeTab === 'home' &&
Home Content
} {activeTab === 'profile' &&
Profile Content
} {activeTab === 'settings' &&
Settings Content
}
) } // Usage function App() { return ( ) } ``` -------------------------------- ### Targeting Operations with IDs and isLast() Source: https://github.com/vitormarkis/saphyra/blob/main/apps/docs/content/docs/features/onfinish-api.mdx Explains the importance of the `id` for uniquely identifying asynchronous operations and how the `isLast()` property in the context can be used to determine if the current operation is the most recent one for a given ID. ```APIDOC ## `id` and `isLast()` in `onFinish` ### Description The `id` property allows multiple `onFinish` callbacks to reference the same asynchronous work. The `isLast()` function, available in the context passed to the `fn` callback, helps manage scenarios where multiple operations with the same ID might be in progress, ensuring actions are taken only for the latest one. ### Example: Using `isLast()` for Conditional Logic ```javascript onFinish({ id: ["todo"], fn: (resolve, reject, { isLast }) => { if (!isLast()) { // If not the last operation, do nothing or return early return; } console.log( '*All* todo related async actions have finished! Now we can fetch most recent data.' ); getNewDataFromAPI() .then(resolve) .catch(reject); } }); ``` ```