React Guide
Use for React feature UI, components, hooks, dependency handling, derived state, and @faasjs/react helpers in FaasJS projects.
Applicable Scenarios
- Creating new React feature UI, components, or hooks
- Reviewing hook usage and dependency handling
- Deciding whether state should be derived, memoized, or synced
- Handling object, array, or function type dependencies
- Choosing between native React hooks and
@faasjs/reacthelpers
Default Workflow
- Keep render logic pure and derive values inline when possible.
- Use event handlers for user-driven updates.
- Avoid native
useEffectby default; useuseEqualEffectonly for real side effects. - Use
useStatesfor component-local state instead of ReactuseState. - Use
useStatesRefwhen component-local state also needs latest-value refs. - Use equal memo hooks for object, array, or function dependencies.
- Reach for state, context, and rendering helpers only when they solve a specific problem.
- Keep data-fetching choices explicit; read React Data Fetching Guide for request flows.
- Receive component inputs as
propsand readprops.xxx; destructure React hook returns at the call site.
Rules
1. Avoid native useEffect by default
Prefer derived values in render instead of mirroring props or state through useEffect. Use event handlers for actions that can run directly from user interaction.
Prefer:
type Props = { firstName: string; lastName: string }
export function DisplayName(props: Props) {
const fullName = `${props.firstName} ${props.lastName}`
return <div>{fullName}</div>
}
Avoid:
import { useEffect, useState } from 'react'
type Props = { firstName: string; lastName: string }
export function DisplayName(props: Props) {
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${props.firstName} ${props.lastName}`)
}, [props.firstName, props.lastName])
return <div>{fullName}</div>
}
2. Use useEqualEffect for real side effects
import { useEqualEffect } from '@faasjs/react'
type Props = { page: number; tags: string[] }
export function Logger(props: Props) {
useEqualEffect(() => {
logger.info('filters changed', { page: props.page, tags: props.tags })
}, [props.page, props.tags])
return <div>Current page: {props.page}</div>
}
- Use instead of native
useEffectin this repo. - Keep responsibilities narrow and side-effect focused.
- Prefer especially when dependencies include objects, arrays, or nested params.
3. Use equal memo hooks for non-primitive dependencies
Use useEqualMemo for computed values with deep dependencies:
import { useEqualMemo } from '@faasjs/react'
type Props = { filters: Record<string, any> }
export function QueryPreview(props: Props) {
const query = useEqualMemo(() => JSON.stringify(props.filters), [props.filters])
return <div>Query: {query}</div>
}
Use useEqualCallback for callbacks with deep dependencies:
import { useEqualCallback } from '@faasjs/react'
type Props = { onSearch: (query: string) => void; filters: Record<string, any> }
export function SearchButton(props: Props) {
const handleSearch = useEqualCallback(
() => props.onSearch(JSON.stringify(props.filters)),
[props.filters, props.onSearch],
)
return <button onClick={handleSearch}>Search</button>
}
Use useEqualMemoize for value stabilization without a computation:
import { useEqualMemoize } from '@faasjs/react'
type Props = { filters: Record<string, any> }
export function SearchPanel(props: Props) {
const stableFilters = useEqualMemoize(props.filters)
return <div>Stable: {JSON.stringify(stableFilters)}</div>
}
- If dependencies are primitives and computation is cheap, inline it.
4. Use useStates for component-local state
useStates: default local state with matching setters.
import { useStates } from '@faasjs/react'
export function SearchBox() {
const { keyword, setKeyword, setPage } = useStates({ keyword: '', page: 1 })
return (
<div>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={() => setPage((page) => page + 1)}>Next page</button>
</div>
)
}
- Prefer
useStatesover ReactuseStatefor component-local state. - Destructure the fields used from
useStatesat the call site. - Group related local fields in one
useStatescall.
5. Use other state helpers only for their specific jobs
useConstant: create one value per component instance.
import { useConstant } from '@faasjs/react'
export function IdDisplay() {
const id = useConstant(() => crypto.randomUUID())
return <div>ID: {id}</div>
}
usePrevious: compare current and previous values when it improves clarity.
import { usePrevious } from '@faasjs/react'
export function CountDisplay(props: { count: number }) {
const prev = usePrevious(props.count)
return (
<div>
Current: {props.count}, Previous: {prev}
</div>
)
}
useStatesRef: async callbacks, timers, subscriptions, or other delayed work need both state and latest-value refs.
import { useStatesRef } from '@faasjs/react'
export function AsyncCounter() {
const { count, setCount, countRef } = useStatesRef({ count: 0 })
const logLater = () => {
setTimeout(() => {
console.log('Count at call time:', countRef.current)
}, 1000)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<button onClick={logLater}>Log later</button>
</div>
)
}
- Prefer
useStatesRefover pairinguseStateswith separate refs when the same local fields need current state and latest-value refs. - Destructure the fields and refs used from
useStatesRefat the call site.
6. Split shared state deliberately
Use createSplittingContext when one state object is shared and its fields change independently. Use useStates to build the provider value with matching setters. Use useStatesRef only when the same provider state also needs latest-value refs. Prefer this over putting a large mutable object into one normal context.
import { createSplittingContext, useStates } from '@faasjs/react'
const CounterContext = createSplittingContext<{
count: number
setCount: (value: number) => void
keyword: string
setKeyword: (value: string) => void
}>(['count', 'setCount', 'keyword', 'setKeyword'])
function Filters() {
const { keyword, setKeyword } = CounterContext.use()
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
}
function Count() {
const { count, setCount } = CounterContext.use()
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
export function Page() {
const { count, setCount, keyword, setKeyword } = useStates({ count: 0, keyword: '' })
return (
<CounterContext.Provider value={{ count, setCount, keyword, setKeyword }}>
<Filters />
<Count />
</CounterContext.Provider>
)
}
7. Use rendering helpers at composition boundaries
OptionalWrapper: same children sometimes need a wrapper and sometimes render directly.
import { OptionalWrapper } from '@faasjs/react'
import { Card } from 'antd'
export function Widget(props: { wrapper?: boolean; children: React.ReactNode }) {
return (
<OptionalWrapper
condition={Boolean(props.wrapper)}
Wrapper={Card}
wrapperProps={{ size: 'small' }}
>
{props.children}
</OptionalWrapper>
)
}
ErrorBoundary: unstable widgets, remote content, or feature islands where local failure should not break the whole page.
import { ErrorBoundary } from '@faasjs/react'
export function SafeWidget(props: { content: string }) {
return (
<ErrorBoundary errorChildren={<div>Something went wrong</div>}>
<div>{props.content}</div>
</ErrorBoundary>
)
}
- Keep fallbacks simple and user-facing.
8. Keep request/client choices explicit
- Use
useFaas,useFaasStream,faas,FaasDataWrapper, andwithFaasDataintentionally. - Configure
FaasReactClientcentrally. - Use
getClientonly for multiple Faas clients. - Read React Data Fetching Guide for detailed request-flow patterns.
Review Checklist
- native
useEffectis not used for normal React logic - derived state is computed inline instead of mirrored through effects
- component-local state uses
useStatesinstead of ReactuseState - component-local state with latest-value refs uses
useStatesRef - React hook return values are destructured at the call site
- object or array dependencies use equal hook variants
- memoization solves real stability or performance needs
- state, context, rendering, request, and client helpers are used for their specific purposes
- data fetching and testing follow the dedicated guides below