diff --git a/.agents/skills/react-useeffect/README.md b/.agents/skills/react-useeffect/README.md new file mode 100644 index 00000000000..6c21cdd17ca --- /dev/null +++ b/.agents/skills/react-useeffect/README.md @@ -0,0 +1,341 @@ +# React useEffect Best Practices + +A comprehensive guide teaching when to use `useEffect` in React, and more importantly, when NOT to use it. This skill is based on official React documentation and provides practical alternatives to common useEffect anti-patterns. + +## Purpose + +Effects are an **escape hatch** from React's reactive paradigm. They let you synchronize with external systems like browser APIs, third-party widgets, or network requests. However, many developers overuse Effects for tasks that React handles better through other means. + +This skill helps you: + +- Identify when you truly need an Effect vs. when you don't +- Recognize common anti-patterns and their fixes +- Apply better alternatives like `useMemo`, `key` prop, and event handlers +- Write Effects that are clean, maintainable, and free from race conditions + +## When to Use This Skill + +Use this skill when you're: + +- Writing or reviewing `useEffect` code +- Using `useState` to store derived values +- Implementing data fetching or subscriptions +- Synchronizing state between components +- Facing bugs with stale data or race conditions +- Wondering if your Effect is necessary + +**Trigger phrases:** + +- "Should I use useEffect for this?" +- "How do I fix this useEffect?" +- "My Effect is causing too many re-renders" +- "Data fetching with useEffect" +- "Reset state when props change" +- "Derived state from props" + +## How It Works + +This skill provides guidance through three key resources: + +1. **Quick Reference Table** - Fast lookup for common scenarios with DO/DON'T patterns +2. **Decision Tree** - Visual flowchart to determine the right approach +3. **Detailed Anti-Patterns** - 9 common mistakes with explanations and fixes +4. **Better Alternatives** - 8 proven patterns to replace unnecessary Effects + +The skill teaches you to ask the right questions: + +- Is there an external system involved? +- Am I responding to a user event or component appearance? +- Can this value be calculated during render? +- Do I need to reset state when a prop changes? + +## Key Features + +### 1. Quick Reference Guide + +Visual table showing the DO/DON'T for common scenarios: + +- Derived state from props/state +- Expensive calculations +- Resetting state on prop change +- User event responses +- Notifying parent components +- Data fetching + +### 2. Decision Tree + +Clear flowchart that guides you from "Need to respond to something?" to the correct solution: + +- User interaction → Event handler +- Component appeared → Effect (for external sync/analytics) +- Derived value needed → Calculate during render (+ useMemo if expensive) +- Reset state on prop change → Key prop + +### 3. Anti-Pattern Recognition + +Detailed examples of 9 common mistakes: + +1. Redundant state for derived values +2. Filtering/transforming data in Effect +3. Resetting state on prop change +4. Event-specific logic in Effect +5. Chains of Effects +6. Notifying parent via Effect +7. Passing data up to parent +8. Fetching without cleanup (race conditions) +9. App initialization in Effect + +Each anti-pattern includes: + +- Bad example with explanation +- Good example with fix +- Why the anti-pattern is problematic + +### 4. Better Alternatives + +8 proven patterns to replace unnecessary Effects: + +1. Calculate during render for derived state +2. `useMemo` for expensive calculations +3. `key` prop to reset state +4. Store ID instead of object for stable references +5. Event handlers for user actions +6. `useSyncExternalStore` for external stores +7. Lifting state up for shared state +8. Custom hooks for data fetching with cleanup + +## Usage Examples + +### Example 1: Derived State + +**Bad - Unnecessary Effect:** + +```tsx +function Form() { + const [firstName, setFirstName] = useState('Taylor') + const [lastName, setLastName] = useState('Swift') + const [fullName, setFullName] = useState('') + + useEffect(() => { + setFullName(firstName + ' ' + lastName) + }, [firstName, lastName]) +} +``` + +**Good - Calculate during render:** + +```tsx +function Form() { + const [firstName, setFirstName] = useState('Taylor') + const [lastName, setLastName] = useState('Swift') + const fullName = firstName + ' ' + lastName // Just compute it +} +``` + +### Example 2: Resetting State + +**Bad - Effect to reset:** + +```tsx +function ProfilePage({ userId }) { + const [comment, setComment] = useState('') + + useEffect(() => { + setComment('') + }, [userId]) +} +``` + +**Good - Key prop:** + +```tsx +function ProfilePage({ userId }) { + return +} + +function Profile({ userId }) { + const [comment, setComment] = useState('') // Resets automatically +} +``` + +### Example 3: Data Fetching with Cleanup + +**Bad - Race condition:** + +```tsx +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + fetchResults(query).then((json) => { + setResults(json) // "hello" response may arrive after "hell" + }) + }, [query]) +} +``` + +**Good - Cleanup flag:** + +```tsx +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + let ignore = false + + fetchResults(query).then((json) => { + if (!ignore) setResults(json) + }) + + return () => { + ignore = true + } + }, [query]) +} +``` + +### Example 4: Event Handler Instead of Effect + +**Bad - Effect watching state:** + +```tsx +function ProductPage({ product, addToCart }) { + useEffect(() => { + if (product.isInCart) { + showNotification(`Added ${product.name}!`) + } + }, [product]) + + function handleBuyClick() { + addToCart(product) + } +} +``` + +**Good - Handle in event:** + +```tsx +function ProductPage({ product, addToCart }) { + function handleBuyClick() { + addToCart(product) + showNotification(`Added ${product.name}!`) + } +} +``` + +## When You DO Need Effects + +Effects are appropriate for: + +- **Synchronizing with external systems** - Browser APIs, third-party widgets, non-React code +- **Subscriptions** - WebSocket connections, global event listeners (prefer `useSyncExternalStore`) +- **Analytics/logging** - Code that needs to run because the component displayed +- **Data fetching** - With proper cleanup (or use your framework's built-in mechanism) + +## When You DON'T Need Effects + +Avoid Effects for: + +1. **Transforming data for rendering** - Calculate at the top level instead +2. **Handling user events** - Use event handlers where you know exactly what happened +3. **Deriving state** - Just compute it: `const fullName = firstName + ' ' + lastName` +4. **Chaining state updates** - Calculate all next state in the event handler +5. **Notifying parent components** - Call the callback in the same event handler +6. **Resetting state** - Use the `key` prop to create a fresh component instance + +## Best Practices + +### 1. Start Without an Effect + +Before adding an Effect, ask: "Is there an external system involved?" If no, you probably don't need an Effect. + +### 2. Prefer Derived State + +If you can calculate a value from props or state, don't store it in state with an Effect updating it. + +### 3. Use the Right Tool + +- Expensive calculation → `useMemo` +- User interaction → Event handler +- Reset on prop change → `key` prop +- External subscription → `useSyncExternalStore` +- Shared state → Lift state up + +### 4. Always Clean Up + +If your Effect subscribes, fetches, or sets timers, return a cleanup function to prevent memory leaks and race conditions. + +### 5. Avoid Effect Chains + +Multiple Effects triggering each other causes unnecessary re-renders and makes code hard to follow. Calculate everything in one place (usually an event handler). + +### 6. Test in Strict Mode + +React 18+ Strict Mode mounts components twice in development to expose missing cleanup. If your Effect breaks, you need cleanup. + +### 7. Consider Framework Solutions + +For data fetching, prefer your framework's built-in solution (Next.js, Remix) or libraries (React Query, SWR) over manual Effects. + +## Reference Files + +This skill includes three detailed reference documents: + +1. **SKILL.md** - Quick reference table and decision tree +2. **anti-patterns.md** - 9 common mistakes with detailed explanations +3. **alternatives.md** - 8 better alternatives with code examples + +## Common Pitfalls + +### Multiple Re-renders + +**Symptom:** Component re-renders many times in quick succession. + +**Cause:** Effect that sets state based on state it depends on, creating a loop. + +**Fix:** Calculate the final value in an event handler or during render. + +### Stale Data + +**Symptom:** UI shows outdated values briefly before updating. + +**Cause:** Using Effect to update derived state causes an extra render pass. + +**Fix:** Calculate derived values during render instead of in state. + +### Race Conditions + +**Symptom:** Fast typing shows results for old queries after new ones. + +**Cause:** Missing cleanup in data fetching Effect. + +**Fix:** Use cleanup flag (`ignore` variable) or AbortController. + +### Runs Twice in Development + +**Symptom:** Effect runs twice on component mount in development. + +**Cause:** React 18 Strict Mode intentionally mounts components twice to expose bugs. + +**Fix:** Add proper cleanup. If it's app initialization that shouldn't run twice, use a module-level guard. + +## Resources + +This skill is based on: + +- [React Official Docs: You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) +- [React Official Docs: Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects) +- [React Official Docs: Lifecycle of Reactive Effects](https://react.dev/learn/lifecycle-of-reactive-effects) + +## Summary + +The golden rule: **Effects are an escape hatch from React.** If you're not synchronizing with an external system, you probably don't need an Effect. + +Before writing `useEffect`, ask yourself: + +1. Is this responding to a user interaction? → Use event handler +2. Is this a value I can calculate from props/state? → Calculate during render +3. Is this resetting state when a prop changes? → Use key prop +4. Is this synchronizing with an external system? → Use Effect with cleanup + +Follow these patterns, and your React code will be more maintainable, performant, and bug-free. diff --git a/.agents/skills/react-useeffect/SKILL.md b/.agents/skills/react-useeffect/SKILL.md new file mode 100644 index 00000000000..83ef6e036e6 --- /dev/null +++ b/.agents/skills/react-useeffect/SKILL.md @@ -0,0 +1,53 @@ +--- +name: react-useeffect +description: React useEffect best practices from official docs. Use when writing/reviewing useEffect, useState for derived values, data fetching, or state synchronization. Teaches when NOT to use Effect and better alternatives. +--- + +# You Might Not Need an Effect + +Effects are an **escape hatch** from React. They let you synchronize with external systems. If there is no external system involved, you shouldn't need an Effect. + +## Quick Reference + +| Situation | DON'T | DO | +| ------------------------------ | ------------------------------ | ------------------------------------- | +| Derived state from props/state | `useState` + `useEffect` | Calculate during render | +| Expensive calculations | `useEffect` to cache | `useMemo` | +| Reset state on prop change | `useEffect` with `setState` | `key` prop | +| User event responses | `useEffect` watching state | Event handler directly | +| Notify parent of changes | `useEffect` calling `onChange` | Call in event handler | +| Fetch data | `useEffect` without cleanup | `useEffect` with cleanup OR framework | + +## When You DO Need Effects + +- Synchronizing with **external systems** (non-React widgets, browser APIs) +- **Subscriptions** to external stores (use `useSyncExternalStore` when possible) +- **Analytics/logging** that runs because component displayed +- **Data fetching** with proper cleanup (or use framework's built-in mechanism) + +## When You DON'T Need Effects + +1. **Transforming data for rendering** - Calculate at top level, re-runs automatically +2. **Handling user events** - Use event handlers, you know exactly what happened +3. **Deriving state** - Just compute it: `const fullName = firstName + ' ' + lastName` +4. **Chaining state updates** - Calculate all next state in the event handler + +## Decision Tree + +``` +Need to respond to something? +├── User interaction (click, submit, drag)? +│ └── Use EVENT HANDLER +├── Component appeared on screen? +│ └── Use EFFECT (external sync, analytics) +├── Props/state changed and need derived value? +│ └── CALCULATE DURING RENDER +│ └── Expensive? Use useMemo +└── Need to reset state when prop changes? + └── Use KEY PROP on component +``` + +## Detailed Guidance + +- [Anti-Patterns](./anti-patterns.md) - Common mistakes with fixes +- [Better Alternatives](./alternatives.md) - useMemo, key prop, lifting state, useSyncExternalStore diff --git a/.agents/skills/react-useeffect/alternatives.md b/.agents/skills/react-useeffect/alternatives.md new file mode 100644 index 00000000000..01fa6b6a4b3 --- /dev/null +++ b/.agents/skills/react-useeffect/alternatives.md @@ -0,0 +1,265 @@ +# Better Alternatives to useEffect + +## 1. Calculate During Render (Derived State) + +For values derived from props or state, just compute them: + +```tsx +function Form() { + const [firstName, setFirstName] = useState('Taylor') + const [lastName, setLastName] = useState('Swift') + + // Runs every render - that's fine and intentional + const fullName = firstName + ' ' + lastName + const isValid = firstName.length > 0 && lastName.length > 0 +} +``` + +**When to use**: The value can be computed from existing props/state. + +--- + +## 2. useMemo for Expensive Calculations + +When computation is expensive, memoize it: + +```tsx +import { useMemo } from 'react' + +function TodoList({ todos, filter }) { + const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]) +} +``` + +**How to know if it's expensive**: + +```tsx +console.time('filter') +const visibleTodos = getFilteredTodos(todos, filter) +console.timeEnd('filter') +// If > 1ms, consider memoizing +``` + +**Note**: React Compiler can auto-memoize, reducing manual useMemo needs. + +--- + +## 3. Key Prop to Reset State + +To reset ALL state when a prop changes, use key: + +```tsx +// Parent passes userId as key +function ProfilePage({ userId }) { + return ( + + ) +} + +function Profile({ userId }) { + // All state here resets when userId changes + const [comment, setComment] = useState('') + const [likes, setLikes] = useState([]) +} +``` + +**When to use**: You want a "fresh start" when an identity prop changes. + +--- + +## 4. Store ID Instead of Object + +To preserve selection when list changes: + +```tsx +// BAD: Storing object that needs Effect to "adjust" +function List({ items }) { + const [selection, setSelection] = useState(null) + + useEffect(() => { + setSelection(null) // Reset when items change + }, [items]) +} + +// GOOD: Store ID, derive object +function List({ items }) { + const [selectedId, setSelectedId] = useState(null) + + // Derived - no Effect needed + const selection = items.find((item) => item.id === selectedId) ?? null +} +``` + +**Benefit**: If item with selectedId exists in new list, selection preserved. + +--- + +## 5. Event Handlers for User Actions + +User clicks/submits/drags should be handled in event handlers, not Effects: + +```tsx +// Event handler knows exactly what happened +function ProductPage({ product, addToCart }) { + function handleBuyClick() { + addToCart(product) + showNotification(`Added ${product.name}!`) + analytics.track('product_added', { id: product.id }) + } + + function handleCheckoutClick() { + addToCart(product) + showNotification(`Added ${product.name}!`) + navigateTo('/checkout') + } +} +``` + +**Shared logic**: Extract a function, call from both handlers: + +```tsx +function buyProduct() { + addToCart(product) + showNotification(`Added ${product.name}!`) +} + +function handleBuyClick() { + buyProduct() +} +function handleCheckoutClick() { + buyProduct() + navigateTo('/checkout') +} +``` + +--- + +## 6. useSyncExternalStore for External Stores + +For subscribing to external data (browser APIs, third-party stores): + +```tsx +// Instead of manual Effect subscription +function useOnlineStatus() { + const [isOnline, setIsOnline] = useState(true) + + useEffect(() => { + function update() { + setIsOnline(navigator.onLine) + } + window.addEventListener('online', update) + window.addEventListener('offline', update) + return () => { + window.removeEventListener('online', update) + window.removeEventListener('offline', update) + } + }, []) + + return isOnline +} + +// Use purpose-built hook +import { useSyncExternalStore } from 'react' + +function subscribe(callback) { + window.addEventListener('online', callback) + window.addEventListener('offline', callback) + return () => { + window.removeEventListener('online', callback) + window.removeEventListener('offline', callback) + } +} + +function useOnlineStatus() { + return useSyncExternalStore( + subscribe, + () => navigator.onLine, // Client value + () => true // Server value (SSR) + ) +} +``` + +--- + +## 7. Lifting State Up + +When two components need synchronized state, lift it to common ancestor: + +```tsx +// Instead of syncing via Effects between siblings +function Parent() { + const [value, setValue] = useState('') + + return ( + <> + + + + ) +} +``` + +--- + +## 8. Custom Hooks for Data Fetching + +Extract fetch logic with proper cleanup: + +```tsx +function useData(url) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let ignore = false + setLoading(true) + + fetch(url) + .then((res) => res.json()) + .then((json) => { + if (!ignore) { + setData(json) + setError(null) + } + }) + .catch((err) => { + if (!ignore) setError(err) + }) + .finally(() => { + if (!ignore) setLoading(false) + }) + + return () => { + ignore = true + } + }, [url]) + + return { data, error, loading } +} + +// Usage +function SearchResults({ query }) { + const { data, error, loading } = useData(`/api/search?q=${query}`) +} +``` + +**Better**: Use framework's data fetching (React Query, SWR, Next.js, etc.) + +--- + +## Summary: When to Use What + +| Need | Solution | +| ------------------------------ | ------------------------------------ | +| Value from props/state | Calculate during render | +| Expensive calculation | `useMemo` | +| Reset all state on prop change | `key` prop | +| Respond to user action | Event handler | +| Sync with external system | `useEffect` with cleanup | +| Subscribe to external store | `useSyncExternalStore` | +| Share state between components | Lift state up | +| Fetch data | Custom hook with cleanup / framework | diff --git a/.agents/skills/react-useeffect/anti-patterns.md b/.agents/skills/react-useeffect/anti-patterns.md new file mode 100644 index 00000000000..2a05839f3b2 --- /dev/null +++ b/.agents/skills/react-useeffect/anti-patterns.md @@ -0,0 +1,289 @@ +# useEffect Anti-Patterns + +## 1. Redundant State for Derived Values + +```tsx +// BAD: Extra state + Effect for derived value +function Form() { + const [firstName, setFirstName] = useState('Taylor') + const [lastName, setLastName] = useState('Swift') + const [fullName, setFullName] = useState('') + + useEffect(() => { + setFullName(firstName + ' ' + lastName) + }, [firstName, lastName]) +} + +// GOOD: Calculate during rendering +function Form() { + const [firstName, setFirstName] = useState('Taylor') + const [lastName, setLastName] = useState('Swift') + const fullName = firstName + ' ' + lastName // Just compute it +} +``` + +**Why it's bad**: Causes extra render pass with stale value, then re-renders with updated value. + +--- + +## 2. Filtering/Transforming Data in Effect + +```tsx +// BAD: Effect to filter list +function TodoList({ todos, filter }) { + const [visibleTodos, setVisibleTodos] = useState([]) + + useEffect(() => { + setVisibleTodos(getFilteredTodos(todos, filter)) + }, [todos, filter]) +} + +// GOOD: Filter during render (memoize if expensive) +function TodoList({ todos, filter }) { + const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]) +} +``` + +--- + +## 3. Resetting State on Prop Change + +```tsx +// BAD: Effect to reset state +function ProfilePage({ userId }) { + const [comment, setComment] = useState('') + + useEffect(() => { + setComment('') + }, [userId]) +} + +// GOOD: Use key prop +function ProfilePage({ userId }) { + return +} + +function Profile({ userId }) { + const [comment, setComment] = useState('') // Resets automatically +} +``` + +**Why key works**: React treats components with different keys as different components, recreating state. + +--- + +## 4. Event-Specific Logic in Effect + +```tsx +// BAD: Effect for button click result +function ProductPage({ product, addToCart }) { + useEffect(() => { + if (product.isInCart) { + showNotification(`Added ${product.name}!`) + } + }, [product]) + + function handleBuyClick() { + addToCart(product) + } +} + +// GOOD: Handle in event handler +function ProductPage({ product, addToCart }) { + function handleBuyClick() { + addToCart(product) + showNotification(`Added ${product.name}!`) + } +} +``` + +**Why it's bad**: Effect fires on page refresh (isInCart is true), showing notification unexpectedly. + +--- + +## 5. Chains of Effects + +```tsx +// BAD: Effects triggering each other +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + const [isGameOver, setIsGameOver] = useState(false) + + useEffect(() => { + if (card?.gold) setGoldCardCount((c) => c + 1) + }, [card]) + + useEffect(() => { + if (goldCardCount > 3) { + setRound((r) => r + 1) + setGoldCardCount(0) + } + }, [goldCardCount]) + + useEffect(() => { + if (round > 5) setIsGameOver(true) + }, [round]) +} + +// GOOD: Calculate in event handler +function Game() { + const [card, setCard] = useState(null) + const [goldCardCount, setGoldCardCount] = useState(0) + const [round, setRound] = useState(1) + const isGameOver = round > 5 // Derived! + + function handlePlaceCard(nextCard) { + if (isGameOver) throw Error('Game ended') + + setCard(nextCard) + if (nextCard.gold) { + if (goldCardCount < 3) { + setGoldCardCount(goldCardCount + 1) + } else { + setGoldCardCount(0) + setRound(round + 1) + if (round === 5) alert('Good game!') + } + } + } +} +``` + +**Why it's bad**: Multiple re-renders (setCard -> setGoldCardCount -> setRound -> setIsGameOver). Also fragile for features like history replay. + +--- + +## 6. Notifying Parent via Effect + +```tsx +// BAD: Effect to notify parent +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + useEffect(() => { + onChange(isOn) + }, [isOn, onChange]) + + function handleClick() { + setIsOn(!isOn) + } +} + +// GOOD: Notify in same event +function Toggle({ onChange }) { + const [isOn, setIsOn] = useState(false) + + function updateToggle(nextIsOn) { + setIsOn(nextIsOn) + onChange(nextIsOn) // Same event, batched render + } + + function handleClick() { + updateToggle(!isOn) + } +} + +// BEST: Fully controlled component +function Toggle({ isOn, onChange }) { + function handleClick() { + onChange(!isOn) + } +} +``` + +--- + +## 7. Passing Data Up to Parent + +```tsx +// BAD: Child fetches, passes up via Effect +function Parent() { + const [data, setData] = useState(null) + return +} + +function Child({ onFetched }) { + const data = useSomeAPI() + + useEffect(() => { + if (data) onFetched(data) + }, [onFetched, data]) +} + +// GOOD: Parent fetches, passes down +function Parent() { + const data = useSomeAPI() + return +} +``` + +**Why**: Data should flow down. Upward flow via Effects makes debugging hard. + +--- + +## 8. Fetching Without Cleanup (Race Condition) + +```tsx +// BAD: No cleanup - race condition +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + fetchResults(query).then((json) => { + setResults(json) // "hello" response may arrive after "hell" + }) + }, [query]) +} + +// GOOD: Cleanup ignores stale responses +function SearchResults({ query }) { + const [results, setResults] = useState([]) + + useEffect(() => { + let ignore = false + + fetchResults(query).then((json) => { + if (!ignore) setResults(json) + }) + + return () => { + ignore = true + } + }, [query]) +} +``` + +--- + +## 9. App Initialization in Effect + +```tsx +// BAD: Runs twice in dev, may break auth +function App() { + useEffect(() => { + loadDataFromLocalStorage() + checkAuthToken() // May invalidate token on second call! + }, []) +} + +// GOOD: Module-level guard +let didInit = false + +function App() { + useEffect(() => { + if (!didInit) { + didInit = true + loadDataFromLocalStorage() + checkAuthToken() + } + }, []) +} + +// ALSO GOOD: Module-level execution +if (typeof window !== 'undefined') { + checkAuthToken() + loadDataFromLocalStorage() +} +``` diff --git a/.claude/skills/react-useeffect b/.claude/skills/react-useeffect new file mode 120000 index 00000000000..05a484a5139 --- /dev/null +++ b/.claude/skills/react-useeffect @@ -0,0 +1 @@ +../../.agents/skills/react-useeffect \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json index 28906ae4dfc..9c66b928571 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -5,6 +5,11 @@ "source": "vercel-labs/agent-browser", "sourceType": "github", "computedHash": "587f7d9187c237d909b9f446cf3ac357184342d54ceb7c430dec56fdbac6816c" + }, + "react-useeffect": { + "source": "softaworks/agent-toolkit", + "sourceType": "github", + "computedHash": "8c24990b50f431a570f3ad1f60632dee154431861547103047f6ce80092ca734" } } }