State Management Hooks in Fre: useState and useReducer Explained
Fre provides React-compatible state management hooks including useState for simple state and useReducer for complex logic, both implemented in src/hook.ts and sharing a unified update mechanism.
Fre is a lightweight alternative to React that implements familiar hooks for managing component state. The library exposes two primary state management hooks—useState and useReducer—which handle everything from simple counters to Redux-style state machines. These hooks are defined in the core hook module and exported through the public API for direct consumption.
Available State Management Hooks
Fre offers two complementary hooks for state management, each serving different complexity needs while sharing the same underlying architecture.
useState for Simple State
The useState hook provides a straightforward way to add reactive state to functional components. It accepts an initial value (or a lazy initializer function) and returns a tuple containing the current state and a setter function.
According to the Fre source code in src/hook.ts:L25-L27, useState is implemented as a thin wrapper around useReducer, passing null as the reducer argument. This design ensures consistency across all state updates while keeping the API surface minimal for simple use cases.
useReducer for Complex Logic
The useReducer hook handles more structured state transitions through a reducer function, following the Redux pattern. It accepts a reducer function and an initial state (or initializer), returning the current state and a dispatch function.
The implementation spans lines src/hook.ts:L29-L51, where the hook manages state transitions by calling the provided reducer with the previous state and the dispatched action. When no reducer is supplied (the useState case), the logic falls back to direct value assignment or functional updates.
Internal Implementation Details
Both hooks rely on a shared internal mechanism that manages hook instances through a slot-based system tied to the fiber architecture.
Slot Allocation via getSlot
Every hook call reserves a position in the current fiber’s hooks.list using a global cursor. The getSlot function at src/hook.ts:L97-L104 handles this allocation, ensuring that each hook maintains its identity across re-renders. On the initial render, the slot is empty; subsequent renders retrieve the existing state from the fiber’s hook list.
The Update Mechanism
State updates trigger re-renders through a unified dispatch flow. When you call the setter (from useState) or dispatch (from useReducer), the internal logic at src/hook.ts:L43-L48 computes the next state:
- If a reducer exists, it invokes
reducer(previous, action) - If no reducer exists (useState), it checks if the value is a function (
isFn(value)) and calls it with the previous state, or uses the value directly - When the new value differs from the current state,
update(current)schedules a re-render of the component fiber
Usage Examples
Basic useState Counter
Import useState from the Fre package to manage simple scalar values:
import { h, render, useState } from 'fre';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
render(<Counter />, document.getElementById('app'));
This example demonstrates lazy functional updates using c => c + 1, which Fre handles internally by detecting function values at src/hook.ts:L43-L48.
Complex State with useReducer
For structured state logic, import useReducer and define your state transitions:
import { h, render, useReducer } from 'fre';
type Action = { type: 'increment' } | { type: 'decrement' };
function counterReducer(state: number, action: Action): number {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
default: return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
The dispatch function triggers the same internal update(current) call used by useState, ensuring consistent re-render behavior across both hooks.
Public API Export
Both hooks are publicly available through the main entry point. As defined in src/index.ts:L4-L6, you can import them directly:
import { useState, useReducer } from 'fre';
Example implementations are also available in the demo directory at [demo/src/use-state.tsx](https://github.com/frejs/fre/blob/master/demo/src/use-state.tsx) and [demo/src/use-reducer.tsx](https://github.com/frejs/fre/blob/master/demo/src/use-reducer.tsx).
Summary
- Fre implements
useStateanduseReducerinsrc/hook.tsas the primary state management hooks, exported throughsrc/index.ts. useStateis a specialized wrapper arounduseReducerthat passesnullas the reducer, delegating to the same internal update logic.- Both hooks use a slot-based allocation system via
getSlotto maintain state across re-renders within the fiber architecture. - State updates schedule re-renders through the
update(current)mechanism when values change, as implemented at lines 43-48 of the hook module. - The API supports both direct value updates and functional updates (
prev => next), with lazy initialization supported for both hooks.
Frequently Asked Questions
What is the difference between useState and useReducer in Fre?
useState is optimized for simple scalar values and direct updates, while useReducer manages complex state objects through action-based transitions. In the Fre codebase, useState actually calls useReducer internally with a null reducer, meaning they share identical performance characteristics and update mechanisms—the difference is purely API ergonomics.
How does Fre implement useState internally?
According to src/hook.ts:L25-L27, useState invokes useReducer with null as the first argument. This design keeps the codebase DRY, ensuring that both hooks benefit from the same slot allocation and comparison logic defined in the shared implementation.
Can I use lazy initialization with Fre's state hooks?
Yes, both hooks support lazy initialization. Pass a function instead of a value to defer expensive computations until the initial render. The initializer runs only once when the slot is first allocated in the fiber's hooks.list, as handled by the slot creation logic at src/hook.ts:L97-L104.
Where are the state management hooks exported from in Fre?
The hooks are defined in src/hook.ts and re-exported from the public API entry point at src/index.ts:L4-L6. Import them directly from the fre package: import { useState, useReducer } from 'fre'.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →