React Setup is a collection of hooks and helpers for better DX and component declaration with explicit setup and render phases.
Overreacting? Tired of jumping through hooks? The library provides instant relief even from severe hook fatigue. This can be considered a lesson in component design learned by React from Vue, Solid, Svelte and other modern frameworks.
Code is worth a thousand words.
const Heroes = props => {
const prepareWorldPromise = useWorld();
use(prepareWorldPromise);
const villains = use(VillainsContext);
const [heroes, setHeroes] = useState();
const victims = useMemo(() => calculatePossibleVictims(villains), []); // 😐
const goVillains = useCallback(() => villains.conspire(), []); // 😕
const gadgets = useRef();
useEffect(() => {
gadgets.current = wearGadgets();
return () => removeGadgets();
}, []);
useEffect(() => {
(async () => {
const newHero = await getHero(props.newId, heroes?.missing);
setHeroes(prev => [...prev, newHero]); // 🥱
})(); // 😣
}, [props.newId]);
useEffect(() => {
if (noSuper(heroes)) {
(async () => {
const newHero = await getSuperHero();
setHeroes(prev => [...prev, newHero]); // 😴
})(); // 😵
}
}, [heroes]); // 😓
const disastersPromise = useMemo(() => villains.getDisasters(), []); // 😟
const [disasters, setDisasters] = useState();
useEffect(() => {
(async () => {
setDisasters(await disastersPromise); // 😒
})(); // 🤢
}, [disastersPromise]);
return (
// 😬
{disasters && <>
<WorldDisasters value={disasters}/>
<VictimsContext value={victims}>
<HeroRoster />
<HeroSaloon onBragging={goVillains} />
</VictimsContext>
</>}
);
}
const Heroes = setupComponent(async props => { // Props object is a stable readonly proxy
const prepareWorldPromise = setupHook(useWorld); // Custom hooks are wrapped with setupHook
setupPromise(prepareWorldPromise); // Uses use() inside and still needs a stable promise
const villains = setupContext(VillainsContext); // A ref to current context value
const heroes = setupStateRef(); // Reactive writable ref that wraps useState
const victims = calculatePossibleVictims(unref(villains)); // Just a constant
const goVillains = () => unref(villains).conspire(); // Stable callback reference
let gadgets; // No ref is needed to store a value
setupOnMounted(() => { // Triggered at the first effect run
gadgets = wearGadgets(); // Side effects still go to the hooks, not setup
});
setupOnUnmounted(() => removeGadgets()); // A cleanup paired with mounted hook
setupEffect(async () => { // One effect with no AIIFE, can return a promise if no cleanup
const newHero = await getHero(props.newId, unref(heroes)?.missing); // Omit .current
heroes.current = heroes => [...heroes, newHero]; // Supports a setter but doesn't need it
if (noSuper(unref(heroes))) {
const newHero = await getSuperHero(gadgets);
heroes.current = [...unref(heroes), newHero]; // No setter, a ref keeps fresh state
}
}, [() => props.newId]); // No need for heroes, deps are refs or getter functions
const disasters = await unref(villains).getDisasters(); // Stable promise with no effort
return () => ( // Called inside suspense boundary, has access to setup context above
// Any react hooks are still allowed here, but rarely ever needed
<>
<WorldDisasters value={disasters}/>
<VictimsContext value={victims}>
<HeroRoster />
<HeroSaloon onBragging={goVillains} />
</VictimsContext>
</>
);
});
React class components use a constructor and this
context to represent an instance, but their lifecycles can't pause for async operations, making them limited and counterintuitive in some applications. Functional components sidelined them to introduce concurrent mode and hook-based API, yet they trade explicit instances for hidden, renderer-managed state. This form of encapsulation can be perceived as magical and requires designing a component in a restricted way by forcing it to obey the rules of hooks.
Because a functional component is just a render function, there's no clear boundary between allocating an instance and producing UI. Everything happens at render time, so holding mutable state or using async/await
for control flow during initialization becomes a less straightforward task. This omission complicates patterns like imperative APIs or async bootstrapping and results in less than perfect DX.
The library's goal is to reintroduce instance-like semantics to functional components while not interfering with the normal work of React hooks and renderer. It draws on other frameworks to solve similar problems and enables the declaration of a distinct setup phase while keeping the render function signature familiar.
useCallback
/useMemo
/useRef
workarounds for constantsasync/await
setup
(blocks with async/await
)async/await
)script
(blocks with #await
)createResource
)useTask$
(blocks with async/await
)await
)async/await
)npm install @react-setup/core @react-setup/hooks
import { setupComponent, setupEffect } from '@react-setup/core';
import { setupStateRef, unref } from '@react-setup/hooks';
export default setupComponent(function Count(props) {
const count = setupStateRef(0);
setupEffect(() => {
const id = setInterval(() => count.current++, props.interval);
return () => clearInterval(id);
}, [() => props.interval]);
return () => <p>{unref(count)}</p>;
});
setupComponent
HelpersetupComponent
is a small convenience helper with clear semantics that wraps a component and separates it into setup and render phases. props
parameter is replaced with a readonly proxy over the original object and a reference to which remains constant throughout the component's lifespan.
The setup phase is executed once before mounting a component and allows providing a perceived persistent instance of a component to a render function. It can optionally be async
to make use of React suspense. All React hooks are wrapped in a way that allows them to be queued and executed on every render to not break the rules of hooks.
The render phase is a pure function that returns JSX to be rendered on every component render. Since it's defined in the scope of the setup function, it has access to the setup context. It can also become stateful and use React hooks as usual, but this defeats the purpose of the setup phase and breaks the separation of concerns.
The setup block has limitations and needs to be structured in a certain order to avoid problems to the component lifecycle:
use()
hook to unwrap promise value results in creating a new component instance and discarding the work of other hooks, so the promises must be stableawait
to not break the rules of hooks. A reliable cleanup is impossible for setup body, any side effects that require it should go to the effects.use()
can be used instead of await
to resolve the promises from the setup block.Setup function can return a promise of render function or a render function can call use()
inside to unwrap the promises from setup scope. This requires wrapping a render function internally with nested Render
component and Suspense
boundary to work with React suspense.
Component with asynchronous setup:
const UserProfile = setupComponent(async props => {
// 1. Promise setup hooks
const users = setupPromise(usersService.usersPromise);
// 2. Other setup hooks and variables
const companyInfoRef = setupContext(CompanyInfoContext);
const currentUserRef = setupMemo(() => users.find(user => user.id === props.id), [() => props.id]);
const [detailsRef, setDetails] = setupState();
setupEffect(async () => {
const details = await usersService.getDetails(props.id)
setDetails(details);
}, [() => props.id]);
console.log('Logged on instantiation');
const totalMessage = translate('Total');
// 3. Awaited promises
const usersStats = await usersService.getStats();
// 4. Render function
return () => (
<div>
<Profile data={unref(currentUserRef)} logo={unref(companyInfoRef).logo} />
<ProfileDetails data={unref(detailsRef)} />
<p>${totalMessage}: {usersStats.count}</p>
</div>
);
});
Roughly desugared to functional component:
const UserProfile = props => {
// 1. Promise setup hooks
const users = use(usersService.usersPromise);
// 2. Other setup hooks and variables
const companyInfoRef = useRef();
companyInfoRef.current = use(CompanyInfoContext);
const currentUserRef = useRef();
currentUserRef.current = useMemo(() => users.find(user => user.id === props.id), [props.id]);
const detailsRef = useRef();
const [details, setDetails] = useState();
currentUserRef.current = details;
useEffect(() => {
(async () => {
const details = await usersService.getDetails(props.id)
setDetails(details);
})();
}, [props.id]);
const instanceRef = useRef();
instanceRef.current = instanceRef.current ?? (() => {
console.log('Logged on instantiation');
const totalMessage = translate('Total');
return { totalMessage };
})();
// 3. Awaited promises
const instancePromiseRef = useRef();
instancePromiseRef.current = instancePromiseRef.current ?? (async () => {
const usersStats = await usersService.getStats();
return { usersStats };
})();
// 4. Render function
const UserProfileRender = useCallback(() => {
const { totalMessage } = instanceRef.current;
const { usersStats } = use(instancePromiseRef.current);
return (
<div>
<Profile data={currentUserRef.current} logo={companyInfoRef.current.logo} />
<ProfileDetails data={detailsRef.current} />
<p>${totalMessage}: {usersStats.count}</p>
</div>
);
}, []);
return (
<Suspense>
<UserProfileRender {...props} />
</Suspense>
);
};
Works similarly to async setup until the first await
. Without suspense, there is no need for additional Render
component. Elements from a render function are returned as is.
Component with synchronous setup:
const UserProfile = setupComponent(props => {
// 1. Promise setup hooks
// ...
// 2. Other setup hooks and variables
// ...
console.log('Logged on instantiation');
const totalMessage = translate('Total');
// 3. Render function
return () => <div>...</div>;
});
Roughly desugared to functional component:
const UserProfile = props => {
// 1. Promise setup hooks
// ...
// 2. Other setup hooks and variables
// ...
const instanceRef = useRef();
instanceRef.current = instanceRef.current ?? (() => {
console.log('Logged on instantiation');
const totalMessage = translate('Total');
return { totalMessage };
})();
// 3. Render function
const { totalMessage } = instanceRef.current;
return <div>...</div>;
};
useSetup
and useConstProps
HooksThese React hooks offer less obtrusive alternative to setupComponent
and are usable without changing component declaration. useSetup
can serve as gradual transition to setupComponent
definitions or on its own to define perceived component instance on first render, e.g., for the use with useImperativeHandle
. useConstProps
returns a stable reference to original props object for use inside useSetup
, a readonly proxy preventing accidental mutation.
Due to natural limitations that setupComponent
doesn't have, useSetup
must return all values used outside it. It's also not intended for asynchronous setup that uses async/await
and returns a promise.
It doesn't serve a good purpose to move use()
or other hooks that return stable values inside useSetup
. If a context isn't expected to change through the component lifetime, use(Context)
can stay outside useSetup
.
Component with useSetup
:
const UserProfile = propsArg => {
const users = use(usersService.usersPromise);
const companyInfo = use(CompanyInfoContext);
const props = useConstProps(propsArg);
const { detailsRef, totalMessage } = useSetup(() => {
const [detailsRef, setDetails] = setupState();
setupEffect(async () => {
const details = await usersService.getDetails(props.id)
setDetails(details);
}, [() => props.id]);
console.log('Logged on instantiation');
const totalMessage = translate('Total');
return { detailsRef, totalMessage };
});
return <div>...</div>;
};
Roughly desugared to the component:
const UserProfile = props => {
const users = use(usersService.usersPromise);
const companyInfo = use(CompanyInfoContext);
const detailsRef = useRef();
const [details, setDetails] = useState();
currentUserRef.current = details;
useEffect(() => {
(async () => {
const details = await usersService.getDetails(props.id)
setDetails(details);
})();
}, [props.id]);
const instanceRef = useRef();
instanceRef.current = instanceRef.current ?? (() => {
console.log('Logged on instantiation');
const totalMessage = translate('Total');
return { totalMessage };
})();
const { totalMessage } = instanceRef.current;
return <div>...</div>;
};
setupHook
and setupRefHook
HelpersThey can wrap React hooks for use inside setupComponent
or useSetup
. They queue callback arguments to run on each render in the same order to follow the rules of hooks. setupHook
returns constant value from the first call of a callback as is. setupRefHook
prewraps callback result with a ref that can change on component updates.
All setup hooks from core package are React hooks wrapped with setupHook
and setupRefHook
, with parameters and return values adapted to the setup phase.
@react-setup/core
Built-in React hooks and their setup hook counterparts:
React hook | Setup hook |
---|---|
use |
setupPromise , setupContext |
useActionState |
setupActionState |
useCallback |
✖ Not needed |
useContext |
setupContext |
useDebugValue |
setupDebugValue |
useDeferredValue |
setupDeferredValue |
useEffect |
setupEffect |
useEffectEvent |
✖ Not needed |
useFormStatus |
setupFormStatus |
useId |
setupId |
useImperativeHandle |
setupImperativeHandle |
useInsertionEffect |
setupInsertionEffect |
useLayoutEffect |
setupLayoutEffect |
useMemo |
setupMemo |
useOptimistic |
setupOptimistic |
useReducer |
setupReducer |
useRef |
setupRef |
useState |
setupState |
useSyncExternalStore |
setupSyncExternalStore |
useTransition |
setupTransition |
Setup helpers:
setupComponent
setupHook
setupRefHook
@react-setup/hooks
Additional React hooks and their setup hook counterparts:
React hook | Setup hook |
---|---|
useAsyncEffect |
setupEffect (core) |
useAsyncInsertionEffect |
setupInsertionEffect (core) |
useAsyncLayoutEffect |
setupLayoutEffect (core) |
useConst |
✖ Not needed |
useConstProps |
✖ Not needed |
useOnMount |
setupOnMount |
useOnMounted |
setupOnMounted |
useOnUnmounted |
setupOnUnmounted |
usePromise |
setupPromise (core) |
useRefFromState |
✖ Not needed |
useSetup |
✖ Not needed |
useStateRef |
setupStateRef |
useSyncRef |
✖ Not needed |
Utility functions:
cloneRef
createObjProxy
createRefLike
createWritableRef
isRef
isStrictRef
isWritableRef
unref
Utility types:
TMaybeRef
TReadonlyRef
TRef
TWritableRef
Contributions are welcome. Feel free to open issues or submit pull requests.
MIT