react-hook-use-cta: (use Call To Action)

GitHub react-hook-use-cta repo linkNPM LicenseNPM VersionJSR VersionTest

An alternative hook for useReducer, and useContext.

Useful for making simple partial state updates, seeing how your state changes over time, and more.

Features
  1. Built-in History Tracking
    • Automatically tracks previous states and changes
    • Maintains initial state reference
    • Makes it easy to implement undo/redo functionality
  2. Type-Safe Action Handlers
    • Actions are strongly typed
    • Payload validation built-in
    • Better IntelliSense support
  3. Default Actions Out of the Box
    • update: Merge new state
    • reset: Reset to initial state
    • replace: Replace entire state
    • replaceInitial: Replace initial state
    • updateInitial: Update initial state

    Example using built-in actions:

    // Using update action
    dispatch.cta.update({ count: 5 });
    
    // Using reset action
    dispatch.cta.reset();
  4. Flexible State Transformations
    • Transform state before updates
    • Compare states to prevent unnecessary renders
    • Hook into state changes with afterActionChange

    Example with transformations:

    const [history, dispatch] = useCTA({
      initial: { count: 0 },
      transform: (state) => ({
        ...state,
        doubled: state.count * 2
      }),
      actions: {
        increment: (history) => ({
          count: history.current.count + 1
        })
      }
    });
  5. Context Integration
    • Easy creation of context providers
    • Simplified state sharing between components
    • Type-safe context consumption

These features make react-hook-use-cta particularly powerful for:

  • Complex state management scenarios
  • Applications requiring state history
  • Cases where type safety is crucial
  • Situations needing built-in state transformations
  • Components sharing state through context

The library provides a more structured and feature-rich approach compared to the basic state management offered by useState and useReducer, while maintaining type safety and reducing boilerplate code.

Let's compare these approaches with clear examples to showcase react-hook-use-cta's advantages:

  1. useState vs useCTA:
    // useState
    const [profile, setProfile] = useState({
      user: {
        id: '',
        name: '',
        email: '',
        preferences: {
          theme: 'light',
          notifications: true
        }
      },
      stats: {
        lastLogin: null,
        loginCount: 0,
        activeProjects: []
      }
    });
    
    const updatePreferences = (newPrefs) => {
      setProfile(prev => ({
        ...prev,
        user: {
          ...prev.user,
          preferences: {
            ...prev.user.preferences,
            ...newPrefs
          }
        }
      }));
    };
    
    const incrementLoginCount = () => {
      setProfile(prev => ({
        ...prev,
        stats: {
          ...prev.stats,
          loginCount: prev.stats.loginCount + 1,
          lastLogin: new Date()
        }
      }));
    };
    
    // useCTA - with history tracking and type safety
    const [history, dispatch] = useCTA({
      initial: {
        user: {
          id: '',
          name: '',
          email: '',
          preferences: {
            theme: 'light',
            notifications: true
          }
        },
        stats: {
          lastLogin: null,
          loginCount: 0,
          activeProjects: []
        } 
      },
      actions: {
        updatePreferences({current}, newPrefs) {
          return {
            user: {
              ...current.user,
              preferences: {
                ...current.user.preferences,
                ...newPrefs
              }
            }
          }
        },
        incrementLoginCount({current},) {
          return {
            stats: {
              ...current.stats,
              loginCount: current.stats.loginCount + 1,
              lastLogin: new Date()
            }
          }
        }
      }
    });
  2. useReducer vs useCTA:
    // useReducer
    const reducer = (state, action) => {
      switch (action.type) {
        case 'update':
          return { ...state, ...action.payload };
        case 'reset':
          return initialState;
        case 'increment':
          return { ...state, count: state.count + 1 };
        default:
          return state;
      }
    };
    const [state, dispatch] = useReducer(reducer, { count: 0 });
    
    // useCTA - built-in actions and type inference
    const [history, dispatch] = useCTA({
      initial: { count: 0 },
      actions: {
        increment(ctaHistory) {
          const { current } = ctaHistory;
          return { count: current.count + 1 };
        }
      }
    });
    // Built-in: dispatch.update(), dispatch.reset(), dispatch.replace()
  3. createContext vs createCTAContext:
    // Regular Context
    const CountContext = createContext(null);
    const CountProvider = ({ children }) => {
      const [count, setCount] = useState(0);
      return (
        <CountContext.Provider value={{ count, setCount }}>
          {children}
        </CountContext.Provider>
      );
    };
    
    // createCTAContext - includes history and type-safe dispatch
    const { CTAProvider, useCTAHistoryContext, useCTADispatchContext } = createCTAContext({
      initial: { count: 0 },
      actions: {
        increment(ctaHistory) {
          const { current } = ctaHistory;
          return { count: current.count + 1 };
        }
      }
    });
  4. Zustand vs createCTASelector
    // Zustand approach
    type ProfileState = {
      user: {
        id: string;
        name: string;
        email: string;
        preferences: {
          theme: 'light' | 'dark';
          notifications: boolean;
        };
      };
      stats: {
        lastLogin: Date | null;
        loginCount: number;
        activeProjects: string[];
      };
      updatePreferences: (prefs: Partial<ProfileState['user']['preferences']>) => void;
      incrementLoginCount: () => void;
      addProject: (projectId: string) => void;
      getNameEmail: () => Pick<ProfileState['user'], 'name' | 'email'>;
    }
    
    const useProfileStore = create<ProfileState>((set, get) => ({
      user: {
        id: '',
        name: '',
        email: '',
        preferences: {
          theme: 'light',
          notifications: true
        }
      },
      stats: {
        lastLogin: null,
        loginCount: 0,
        activeProjects: []
      },
      getNameEmail: () =>({
        name: get().user.name,
        email: get().user.email
      }),
      updatePreferences: (prefs) => set((state) => ({
        user: {
          ...state.user,
          preferences: {
            ...state.user.preferences,
            ...prefs
          }
        }
      })),
      incrementLoginCount: () => set((state) => ({
        stats: {
          ...state.stats,
          loginCount: state.stats.loginCount + 1,
          lastLogin: new Date()
        }
      })),
      addProject: (projectId) => set((state) => ({
        stats: {
          ...state.stats,
          activeProjects: [...state.stats.activeProjects, projectId]
        }
      }))
    }));
    
    const nameEmail = useProfileStore(useShallow((state) => state.getNameEmail()));
    
    // react-hook-use-cta approach with history tracking and type safety
    const useProfileCTA = createCTASelector({
      initial: {
        user: {
          id: '',
          name: '',
          email: '',
          preferences: {
            theme: 'light' as 'light' | 'dark',
            notifications: true
          }
        },
        stats: {
          lastLogin: null as Date | null,
          loginCount: 0,
          activeProjects: [] as string[]
        }
      },
      actions: {
        updatePreferences({ current }, payload: Partial<typeof current.user.preferences>) {
          return {
            user: {
              ...current.user,
              preferences: {
                ...current.user.preferences,
                ...payload
              }
            }
          };
        },
        incrementLoginCount({ current }) {
          return {
            stats: {
              ...current.stats,
              loginCount: current.stats.loginCount + 1,
              lastLogin: new Date()
            }
          };
        },
        addProject({ current }, projectId: string) {
          return {
            stats: {
              ...current.stats,
              activeProjects: [...current.stats.activeProjects, projectId]
            }
          };
        }
      },
      createFunc(dispatch) {
        return {
          getNameEmail() {
            const { current } = dispatch.history;
            return {
              name: current.user.name,
              email: current.user.email,
            };
          }
        }
      }
    });
    
    const nameEmail = useProfileCTA(({dispatch}) => dispatch.func.getNameEmail());

Install

react-hook-use-cta

NPM

npm i react-hook-use-cta

Yarn

yarn add react-hook-use-cta

Deno

deno add jsr:@rafde/react-hook-use-cta

useCTA

import { useCTA, } from 'react-hook-use-cta';

export function FC() {
	const [
		history,
		dispatch,
	] = useCTA({
		initial: {
			search: '',
		},
	});
	
	return <>
		{history.current.search}
	<>;
}

useCTA Basic Example

1st Parameter: props

props.initial

props.onInit

Optional callback that is a related parameter for:

where Initial is the initial state structure.

Has the following key features:

  • Runs once on component mount
  • Perfect for setting up derived state or initial data from props
  • Useful when you need to perform calculations or transformations on your initial state before your component starts using it.
props.onInit Example

props.compare

Optional callback that is a related parameter for:

  1. previousValue: A state property value from current.
  2. nextValue: new value sent from calling an action.
  3. extra: object containing

It should return:

  • true if the values are considered equal
  • false if the values are different

This is particularly useful when:

  • You need custom equality logic for specific state properties.
  • You want to optimize re-renders by comparing only specific properties.
  • Working with complex nested objects that need special comparison handling.
props.compare Example

props.transform

Optional callback that is a related parameter for:

This callback is an alternative to props.actions. It can read the result of all actions and is useful when:

  • transforming all action CTAState results from a single point.
  • you don't want to override every built-in action to do the same changes to CTAState.
  • adding custom validation.
  • adding side effects.

The order this works is as follows

  • If custom or overridden action from actions is defined, call it. Otherwise, continue.
  • If transform returns
    • undefined: don't trigger action.
    • CTAState or Partial<CTAState>: continue triggering action.
props.transform Example

props.transform Parameters

  1. nextState: CTAState orPartial<CTAState>. It depends the actionType it is behaving like.
  2. transformCTAHistory: CTAHistory with two extra keys:
    • actionType: The name of the built-in action type it will behave like.
    • customAction: The name of the custom action that called it. Otherwise, undefined.

props.transform return

Return value can be:

  • CTAState
  • Partial<CTAState>
  • undefined: action will not be triggered.

depending on the transformCTAHistory.actionType value.

props.afterActionChange

props.afterActionChange Example

props.afterActionChange Parameters

props.afterActionChange return

There's no return value.

props.actions

Overridable built-in actions

All built-in call-to-actions (CTA) can have their behaviors extended or modified.

This pattern enables:

  • Adding custom validation.
  • Transforming data.
  • Adding side effects.

Overridable actions Example

Overridable actions Parameter: CTAHistory

The first parameter every overridable action receives.

Contains the following properties:

  • current: The current hook state.
  • previous: The previous current state object before it was last updated.
    Starts of as null until current state is updated.
  • changes: Values from current state that differ from values in initial state.
    Is null if the there are no differences between initial and current state.

    Useful for tracking changes to state properties and to send only the changes to an API.

  • initial: Typically, the state the hook was initialized with. Can be updated to reflect a point of synchronization.
  • previousInitial: The previous initial state object before it was last updated.
    Starts of as null until initial state is updated.

Parameter: actions?.update

Optional
Called by dispatch.cta.update

payload will update specific properties of CTAHistory.current while preserving other properties values.

Overriding lets you return:

  • Partial<Payload>
  • undefined: Useful when you want to conditionally trigger the action.

Parameter: actions?.replace

Optional

Called by dispatch.cta.replace

payload will replace all properties in CTAHistory.current with new property values.

Overriding lets you return:

  • Payload
  • undefined: Useful when you want to conditionally trigger the action.

Parameter: actions?.reset

Optional

Called by dispatch.cta.reset

Normally when dispatch.cta.reset() is called, it resets the CTAHistory.current state back to the CTAHistory.initial state. But overriding the action allows you to handle the reset in a different way.

payload can be the following:

  • undefined: Handle how you want CTAHistory.current and CTAHistory.initial to be set when no payload is sent.
  • payload: sets CTAHistory.current and CTAHistory.initial state to equal payload.

Overriding lets you return:

  • Payload
  • undefined: Useful when you want to conditionally trigger the action.

Parameter: actions?.updateInitial

Optional

Called by dispatch.cta.updateInitial

payload willupdate specific properties of CTAHistory.initial while preserving other properties values.

Overriding lets you return:

  • Partial<Payload>
  • undefined: Useful when you want to conditionally trigger the action.

Parameter: actions?.replaceInitial

Optional

Called by dispatch.cta.replaceInitial
payload willreplace all properties in CTAHistory.initial with new property values.

Overriding lets you return:

  • Payload
  • undefined: Useful when you want to conditionally trigger the action.

Custom actions

Custom actions are a powerful way to extend the functionality of your state management system. You can define as many as you need.

This gives you the flexibility to:

  • Create domain-specific actions.
  • Encapsulate complex state updates.
  • Build reusable action patterns.
  • Handle specialized business logic.

They are defined as a Record of functions, where the:

  • key: can be a string | number as the action name
  • value: is a function that accepts any number of type declared parameters.

Custom actions Example

Custom action Parameter: CustomCTAHistory

The first parameter of the function is read-only CustomCTAHistory which extends from CTAHistory and gives you access to all the built-in action behaviors.

By default, custom actions behave as an update, but you can customize them to behave like any other built-in action through CustomCTAHistory.

Custom actions Parameters: ...args

Optional

Custom actions can have any number of type declared ...args after the CustomCTAHistory parameter.

These ...args must have their type declared to ensure type safety.

...args will come from calling dispatch.cta.YourCustomAction

Custom actions return value

Custom actions can return several different types of values, depending on action type you want it to behave like.

  • undefined: Action will not be triggered. Useful when you want to conditionally trigger an action.
  • Partial<CTAState>: update specific properties of CTAHistory.current while preserving other properties values. It will use using overridden update action.
  • CustomCTAHistory.updateAction( Partial<CTAState>, { useDefault?: boolean } | undefined, )
    :update specific properties of CTAHistory.current while preserving other properties values.
    { useDefault: true } will bypass the overridden update action behavior.
  • CustomCTAHistory.replaceAction( CTAState, { useDefault?: boolean } | undefined, )
    : replace all properties in CTAHistory.current with new property values.
    { useDefault: true } will bypass the overridden replace action behavior.
  • CustomCTAHistory.resetAction( CTAState | undefined, { useDefault?: boolean } | undefined, )
    : Behaves like an reset action.
    { useDefault: true } will bypass the overridden reset action behavior.
  • CustomCTAHistory.updateInitialAction( Partial<CTAState> | undefined, { useDefault?: boolean } | undefined, )
    : update specific properties of CTAHistory.initial while preserving other properties values.
    { useDefault: true } will bypass the overridden updateInitial action behavior.
  • CustomCTAHistory.replaceInitialAction( CTAState | undefined, { useDefault?: boolean } | undefined, )
    : replace all properties in CTAHistory.initial with new property values.
    { useDefault: true } will bypass the overridden replaceInitial action behavior.

Note: If you have overridden a built-in action, the custom action will use the overridden action.

Sending { useDefault: true } will bypass the overridden action and behave using built-in action behavior.

2nd Parameter: createFunc

Useful for creating async or sync functions that provide reusable state derivations and action compositions.

function parameters must havetypes declared to ensure type safety.

The results are set in dispatch.func

createFunc Example

createFunc Parameter: dispatch

Gives you access to dispatch.historyand dispatch.cta for use by the results of createFunc.

createFunc return

Should return an object with functions that can be sync or async.

Results end up in dispatch.func

useCTA return

useCTA returns a type-safe array with two elements:

  1. CTAHistory<Initial>: Maintains hook state history and change tracking.
  2. dispatch: Dispatch function to trigger actions.

useCTA return value [0]: history

If a call-to-action is successful, it will return a CTAHistory .
If an action returns undefined or the payload does not produce a state change, this value's reference will not change since re-render was not triggered.

useCTA return value [1]: dispatch

Gives you access to the dispatch function which allows you to trigger state history changes through actions.
Re-render will not occur if the state does not change or if the callback returns undefined.

The following actions are available:

dispatch.cta.update

dispatch.cta.update( keyof CTAState, CTAState[keyof CTAState] );

dispatch.cta.update( Partial<CTAState> );

dispatch.cta.update( ( ctaHistory: CTAHistory<CTAState> ) => Partial<CTAState> | undefined );
Alternate dispatch.cta.update
dispatch( {
	type: 'update',
	payload: Partial<CTAState>
} );

dispatch( {
	type: 'update',
	payload: ( ctaHistory: CTAHistory<CTAState> ) => Partial<CTAState> | undefined
} );

dispatch.cta.replace

dispatch.cta.replace( CTAState );

dispatch.cta.replace( ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined );
Alternate dispatch.cta.replace
dispatch( {
	type: 'replace',
	payload: CTAState
} );

dispatch( {
	type: 'replace',
	payload: ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined
} );

dispatch.cta.reset

// Reset the state to the initial state
dispatch.cta.reset();

// sets the current state and initial state to payload
dispatch.cta.reset( CTAState );

// sets the current state and initial state to what is returned from the callback
// if the callback returns undefined, the state will not change
dispatch.cta.reset( ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined );
Alternate dispatch.cta.reset
// Reset the state to the initial state
dispatch( {
	type: 'reset',
} );

// sets the current state and initial state to payload
dispatch( {
	type: 'reset',
	payload: CTAState
} );

// sets the current state and initial state to what is returned from the callback
// if the callback returns undefined, the state will not change
dispatch( {
	type: 'reset',
	payload: ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined
} );

dispatch.cta.updateInitial

dispatch.cta.updateInitial( Partial<CTAState> );

dispatch.cta.updateInitial( ( ctaHistory: CTAHistory<CTAState> ) => Partial<CTAState> | undefined );
Alternate dispatch.cta.updateInitial
dispatch( {
	type: 'updateInitial',
	payload: Partial<CTAState>
} );

dispatch( {
	type: 'updateInitial',
	payload: ( ctaHistory: CTAHistory<CTAState> ) => Partial<CTAState> | undefined
} );

dispatch.cta.replaceInitial

dispatch.cta.replaceInitial( CTAState );

dispatch.cta.replaceInitial( ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined );
Alternate dispatch.cta.replaceInitial
dispatch( {
	type: 'replaceInitial',
	payload: CTAState
} );

dispatch( {
	type: 'replaceInitial',
	payload: ( ctaHistory: CTAHistory<CTAState> ) => CTAState | undefined
} );

dispatch.cta.YourCustomAction

dispatch.cta.YourCustomActionWithoutArgs();

dispatch.cta.YourCustomActionWithArgs( 
	Payload,
	...any[] | undefined
 );
Alternate dispatch.cta.YourCustomAction
dispatch( {
	type: 'YourCustomActionWithoutArgs',
} );

dispatch( {
	type: 'YourCustomActionWithArgs',
	payload: Payload,
	args: any[] | undefined,
} );
YourCustomAction is a placeholder for the name of a custom action you defined in Custom actions

dispatch.history

A read-only reference to the state history, in case you need to read it from somewhere that doesn't need as a dependency.

dispatch.func

createCTASelector

A function that creates a selector hook for managing state with CTA (Call To Action) pattern.

Works similar to Zustand, but with a different API.

import { createCTASelector, } from 'react-hook-use-cta';

const useCTASelector = createCTASelector({
	initial: {
		search: '',
	}
});

export default useCTASelector;

createCTASelector Example

createCTASelector Parameters

createCTASelector return useCTASelector

A selector hook that enables efficient state updates through selector pattern. It also gives you access to:

  • dispatch to trigger actions external to a component.
  • getHistory gives you access toCTAHistory

useCTASelector Parameter:selector

Optional

  • function: Sending a callback enables selecting specific state values, actions, or computed values. It receives an object with dispatch prop and all props in CTAHistory.
  • undefined Calling without sending a function will return UseCTAReturnType

useCTASelector return

returns stable and consistent memoized values. This makes it capable of returning computed objects or other references without worrying about infinite re-renders.

createCTA

import { createCTA, } from 'react-hook-use-cta';

const ctaValue = createCTA({
	initial: {
		search: '',
	}
});

export history = ctaValue[0];
export dispatch = ctaValue[1];
A function that provides a way to execute like useCTA but outside of a React component.

Useful if you want to handle state history and dispatch using a 3rd party global state management system.

createCTA zustand Example

createCTA Parameter

createCTA return values

// Example
const updateWithPayload: CTAHistory<CTAState> = dispatch.cta.update( Partial<CTAState> );

createCTAContext

import { createCTAContext, } from react-hook-use-cta;

const context = createCTAContext({
	initial: {
		search: '',
	}
});

export const Provider = context.Provider;
export const useCTAHistoryContext = context.useCTAHistoryContext;
export const useCTADispatchContext = context.useCTADispatchContext;

This handles the boilerplate of creating a React Context and Provider.

function that returns a Provider, CTAHistory, and dispatch from React Context. Provider will internally setup useCTA to be used through useCTAHistoryContext anduseCTADispatchContext.

createCTAContext Example

createCTAContext Parameters

createCTAContext return value

returnCTAParameter

import { returnCTAParameter, } from 'react-hook-use-cta';

export const ctaParams = returnCTAParameter({
	initial: {
		search: '',
	},
});

export types

Typescript export types in case you need to use them.

CTAState

import type { CTAState, } from 'react-hook-use-cta';

Typescript:

UseCTAParameterCompare

import type { UseCTAParameterCompare, } from 'react-hook-use-cta';

Typescript:

UseCTAReturnType

import type { UseCTAReturnType, } from 'react-hook-use-cta';

Typescript:

UseCTAReturnTypeDispatch

import type { UseCTAReturnTypeDispatch, } from 'react-hook-use-cta';

Typescript: