Skip to content

A React hook for managing complex state with custom actions, history tracking, and type safety.

License

Notifications You must be signed in to change notification settings

rafde/react-hook-use-cta

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

A somewhat flexible react hook alternative to React.useReducer. Written in Typescript.

Table of Contents

Table of Contents: Typescript helper and exports

Installation

react-hook-use-cta

NPM

npm i react-hook-use-cta

Yarn

yarn add react-hook-use-cta

useCTA

Playground

Basic example code
import { useEffect, } from 'react';
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: {
			search: 'initial',
			isFuzzy: false, 
			count: 0,
		}
	});

	useEffect(
		() => dispatch.cta.update('search', 'update search'),
		[]
	);

	/* Renders `update search` */
	return state.search;
}
Advance example code
import { useEffect, } from 'react';
import { useCTA, CustomCTAStateParam, CTAStateParam, } from 'react-hook-use-cta'

type ViewPropsInitial = { 
	search: string
	isFuzzy: boolean
	count: number
};

function View(props: { initial: ViewPropsInitial }) {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: props.initial,
		onInit(initial) {
			return {
				...initial,
				search: 'onInit',
			}
		},
		actions: {
			// START: augment predefined actions
			updateInitial(ctaStateParam: CTAStateParam<ViewPropsInitial>, payload) {
				return payload;
			},
			reset(ctaStateParam: CTAStateParam<ViewPropsInitial>, payload) {
				return payload;
			},
			update(ctaStateParam: CTAStateParam<ViewPropsInitial>, payload) {
				return payload;
			},
			// END: augment predefined actions

			// START: Custom actions
			toggleIsFuzzy(customCTAStateParam: CustomCTAStateParam<ViewPropsInitial>, isFuzzy?: boolean) {
				if (typeof isFuzzy === 'undefined') {
					return {
						isFuzzy: !ctaParam.previous.isFuzzy,
					}
				}

				return {
					isFuzzy
				}
			},
			addToCount(customCTAStateParam: CustomCTAStateParam<ViewPropsInitial>, value: number) {
				return {
					count: ctaParam.previous.count + value,
				}
			},
			incrementCount(customCTAStateParam: CustomCTAStateParam<ViewPropsInitial>) {
				return {
					count: ctaParam.previous.count + 1,
				}
			},
			// END: Custom actions
		}
	});

	useEffect(
		() => dispatch.cta.update('search', 'update'),
		[]
	);

	return <>
		<div>{state.search}</div>
		<div>{dispatch.state.previous.search}</div>
		<div>{dispatch.state.initial.search}</div>
		<div>{dispatch.state.previousInitial?.search}</div>
		<div>{dispatch.state.changes?.search}</div>
	</>;
}

Parameter

export type UseCTAParameter<
Initial extends CTAInitial,
Actions extends UseCTAParameterActionsRecordProp<Initial> | undefined,
> = {
actions?: Actions
initial: Initial
onInit?: ( ( initial: Initial ) => Initial )
};

Note

useCTA accepts an object, that is read once, with the following properties:

initial

Important

A required object representing the initial state. Property values can be anything that strictDeepEqual from fast-equals supports.

Typescript:

onInit

onInit?: ( ( initial: Initial ) => Initial )

Note

An optional callback for setting initial object on first render. It accepts the initial state object and returns a new initial state object.

onInit example code
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
	] = useCTA({
		initial: {
			search: '',
		},
		onInit(initial) {
			return {
				...initial,
				search: 'onInit',
			}
		}
	});
	
	// renders `onInit`
	return state.search;
}

actions

Note

An optional object for augmenting call to actions or to create your own custom call to actions


Return

export type UseCTAReturnType<
Initial extends CTAInitial,
Actions extends UseCTAParameterActionsRecordProp<Initial> | undefined = undefined,
> = [
Initial, // current state
UseCTAReturnTypeDispatch<Initial, Actions>, // dispatcher
];

Note

An array with 2 values:

Current State

Note

A read-only object that is set by initial or result of onInit on first render. It is changed by most call to actions

Dispatcher

Note

A function used to make changes to the current state and trigger re-render. It also includes two properties:

cta

Note

A read-only object for accessing calls to actions to trigger state change. By default, it includes the following calls to actions

replace(
payload: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
),
options?: OptionsParams,
): void
replaceInitial(
payload: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
),
options?: OptionsParams,
): void
reset(
payload?: undefined,
options?: OptionsParams,
): void
reset(
payload?: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
),
options?: OptionsParams,
): void
update(
payload: Partial<Initial> | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Partial<Initial> | undefined
),
options?: OptionsParams,
): void
update(
key: keyof Initial,
value: Initial[keyof Initial],
options?: OptionsParams,
): void
type CTAPayloadCallbackParameter has the following properties:
initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null

state

initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null

Note

A read-only object that can be used to reference additional states: You have access to the following states

state.current

current: Readonly<Initial>

Note

Equivalent to current state.

The following call to actions can affect it:
state.previous

previous: Readonly<Initial>

Note

Starts off as null, is set to the previous state.current by the following actions:

The following call to actions can affect it:
state.initial

initial: Readonly<Initial>

Note

Starts of equal to initial parameter or result of onInit on first render

The following call to actions can affect it:
state.previousInitial

Note

Starts off as null, is set to the previous state.initial by the following actions:

Affecting actions:
state.changes

changes: Readonly<Partial<Initial>> | null

Note

Starts of equal to null. When the property values of state.initial are equal to the current state, the value is null. Otherwise, it's equal to the difference in property values of state.initial and current state.

The following call to actions can affect it:

Dispatcher Parameter

Dispatcher function also accepts a parameter object with properties:

type

Important

Required string. The value is a call to action or custom action name.

payload

Warning

A parameter that a call to action can read. It's value depends on what it's corresponding call to action can accept.

args

options?: Readonly<OptionsParams>

Note

Optional unknown[] an augmented call to action or custom action based on how the action was defined.


Call to Actions

Note

Call to actions can be made through cta or dispatcher and augmented through actions parameter. There are call to actions available for immediate use.

Important

When augmenting an existing call to action, the first parameter signature is CTAStateParam with the following properties:

initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null
options?: Readonly<OptionsParams>
The second parameter depends on what the corresponding call to action expects.

Important

When using a callback as a payload, the first parameter signature is CTAPayloadCallbackParameter with the following properties:

initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null
return; or return undefined results in the call to action not triggering re-render. Otherwise, the returning value depends on what the corresponding call to action expects.

update

Note

Used to partially update the current state with a payload. Affects the following states:

state new state
state.current payload merged with old state.current
state.previous old state.current
state.initial no change
state.previousInitial no change
state.changes difference between state.initial and new state.current or null if equal

How to update a single property

update(
key: keyof Initial,
value: Initial[keyof Initial],
options?: OptionsParams,
): void

update a single property example code
dispatch.cta.update('search', 'update without option');
update a single property with an option example code
dispatch.cta.update('search', 'update with option', {hasOption: true});

How to update multiple properties

update(
payload: Partial<Initial> | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Partial<Initial> | undefined
),
options?: OptionsParams,
): void

update multiple properties example code
dispatch.cta.update({
	search: 'dispatch.cta.update',
	isFuzzy: true, 
});
update multiple properties with an option example code
dispatch.cta.update(
	{
		search: 'dispatch.cta.update with options',
		isFuzzy: true,
	},
	{
		updateWithOption: true,
	}
);
update multiple properties using a callback example code
dispatch.cta.update(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent an update from triggering.
			return;
		}
		
		return {
			search: 'dispatch.cta.update with callback',
			count: ctaPayloadCallbackParameter.current.count + 1,
		}
	}
);
update multiple properties with an option using a callback example code
dispatch.cta.update(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent an update from triggering.
			return;
		}

		return {
			search: 'dispatch.cta.update with callback and options',
			count: ctaPayloadCallbackParameter.current.count + 1,
		}
	},
	{
		updateWithCallbackOption: true,
	}
);
Using dispatcher function instead of dispatch.cta.update

type UpdateCTAProps<Initial extends CTAInitial,> = {
type: 'update'
payload: Partial<Initial> | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Partial<Initial> | undefined
)
options?: OptionsParams
};

dispatch({
	type: 'update',
	payload: {
		search: 'dispatch update',
		isFuzzy: true,
	},
});

dispatch({
	type: 'update',
	payload: {
		search: 'dispatch update with options',
		isFuzzy: true,
	},
	options: {
		dispatchUpdateWithOption: true,
	}
});

dispatch({
	type: 'update',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	payload(ctaPayloadCallbackParameter) {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent an update from happening.
			return;
		}
	
		return {
			search: 'dispatch.cta.update with callback',
			count: ctaPayloadCallbackParameter.current.count + 1,
		}
	},
});


dispatch({
	type: 'update',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	payload: (ctaPayloadCallbackParameter) => ({
		search: 'dispatch update with callback and options',
		count: ctaPayloadCallbackParameter.current.count + 1,
	}),
	options: {
		dispatchUpdateWithCallbackAndOption: true,
	}
});

How to augment update

update?: ( ctaState: CTAStateParam<Initial>, payload: Partial<Initial> ) => Partial<Initial> | undefined

augment update example code
import {useEffect} from 'react';
import {useCTA, CTAStateParam,} from 'react-hook-use-cta'

const initial = {
	search: 'initial',
	isFuzzy: false,
	count: 0,
}

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial,
		actions: {
			/**
			 * @param {CTAStateParam<typeof initial>} ctaStateParam
			 * @param {typeof initial} payload
			 * @returns {(Partial<typeof initial> | void)} returning `void` prevents action from triggering.
			 */
			update(ctaStateParam, payload,) {
				const {
					current,
					options,
				} = ctaStateParam;
				let {
					count,
				} = payload;
				
				if (!Number.isSafeInteger(count)) {
					// if `count` is not a safe integer, prevent update 
					return;
				}

				// set count to current.count if allowNegativeCount is falsey and count is less than 0
				if (count < 0 && !options?.allowNegativeCount) {
					count = current.count;
				}

				return {
					...payload,
					count
				};
			}
		}
	});

	useEffect(
		() => {
			dispatch.cta.update(
				'count',
				-1,
				{
					allowNegativeCount: true
				}
			);
		},
		[
			dispatch,
		]
	);
	
	// will render `-1`
	return state.count;
}

updateInitial

Note

Set a new initial state with a payload. The idea of this action is in case there is new source data that should be used to compare changes with current state Affects the following states:

state new state
state.current no change
state.previous no change
state.initial payload
state.previousInitial old state.initial
state.changes difference between new state.initial and state.current or null if equal

How to call updateInitial

replaceInitial(
payload: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
),
options?: OptionsParams,
): void

updateInitial state example code
dispatch.cta.updateInitial({
	search: 'dispatch.cta.updateInitial',
	isFuzzy: true,
	count: 10,
});
updateInitial state using an option example code
dispatch.cta.updateInitial(
	{
		search: 'dispatch.cta.updateInitial with option',
		isFuzzy: true,
		count: 10,
	},
	{
		isReplaceInitialWithOption: true,
	}
);
updateInitial state using a callback example code
dispatch.cta.updateInitial(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent updateInitial from triggering.
			return;
		}
		
		return {
			search: 'dispatch.cta.updateInitial with callback',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count,
		}
	}
);
updateInitial state with option using a callback example code
dispatch.cta.updateInitial(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent updateInitial from triggering.
			return;
		}

		return {
			search: 'dispatch.cta.updateInitial with callback with options',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count,
		}
	},
	{
		isReplaceInitialCallbackWithOption: true,
	}
);
Using dispatcher function instead of dispatch.cta.updateInitial

type ReplaceInitialCTAProps<Initial extends CTAInitial,> = {
type: 'replaceInitial'
payload: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
)
options?: OptionsParams
};

dispatch({
	type: 'updateInitial',
	payload: {
		search: 'dispatch updateInitial',
		isFuzzy: true,
		count: 10,
	}
});

dispatch({
	type: 'updateInitial',
	payload: {
		search: 'dispatch updateInitial with options',
		isFuzzy: true,
		count: 10,
	},
	options: {
		isReplacingWithOption: true,
	}
});

dispatch({
	type: 'updateInitial',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	payload(ctaPayloadCallbackParameter) {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// This is a way to prevent updateInitial from triggering.
			return;
		}
	
		return {
			search: 'dispatch.cta.updateInitial with callback',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count,
		}
	},
});

dispatch({
	type: 'updateInitial',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	payload: (ctaPayloadCallbackParameter) => ({
		search: 'dispatch updateInitial with callback and options',
		isFuzzy: true,
		count: ctaPayloadCallbackParameter.current.count + 1,
	}),
	options: {
		isCallbackReplacingWithOption: true,
	}
});

How to augment updateInitial

replaceInitial?: ( ctaState: CTAStateParam<Initial>, payload: Initial ) => Initial | undefined

augment updateInitial example code
import {useEffect} from 'react';
import {useCTA, CTAStateParam,} from 'react-hook-use-cta'

const initial = {
	search: 'initial',
	isFuzzy: false,
	count: 0,
}

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial,
		actions: {
			/**
			 * @param {CTAStateParam<typeof initial>} ctaStateParam
			 * @param {typeof initial} payload
			 * @returns {(typeof initial | undefined)} returning `undefined` prevents action from triggering.
			 */
			updateInitial(ctaStateParam, payload) {
				const {
					current,
					options,
				} = ctaStateParam;
				let {
					count,
				} = payload;

				if (Number.isSafeInteger(count)) {
					// prevent updateInitial if count is not a safe integer
					return;
				}

				// set count to current.count if allowNegativeCount is falsey and count is less than 0
				if (count < 0 && !options?.allowNegativeCount) {
					count = current.count;
				}

				return {
					...payload,
					count
				};
			}
		}
	});

	useEffect(
		() => {
			dispatch.cta.updateInitial(
				{
					search: 'updateInitial',
					isFuzzy: true,
					count: 10,
				},
				{
					allowNegativeCount: true,
				}
			);
		},
		[
			dispatch,
		]
	);
	
	// will render `10`
	return dispatch.state.initial.count;
}

reset

Note

reset is a special action that has 2 behaviors:

How to call reset without a payload to replace current state with initial state

reset(
payload?: undefined,
options?: OptionsParams,
): void

Note

If no payload is sent, then the current state will be replaced the initial state. Affects the following states:

state new state
state.current state.initial
state.previous old state.current
state.initial no change
state.initial no change
state.changes null since state.initial equals state.current
reset state example code
// sets current state = to initial state
dispatch.cta.reset();
reset state example code using an option
// sets current state = to initial state
dispatch.cta.reset(
	undefined,
	{
		resetWithOption: true,
	}
);
Using dispatcher function instead of dispatch.cta.reset without payload

type ResetCTAProps<Initial extends CTAInitial,> = {
type: 'reset'
payload?: Initial | (
( ctaState: UseCTAReturnTypeDispatchState<Initial> ) => Initial | undefined
)
options?: OptionsParams
};

dispatch({
	type: 'reset'
});

dispatch({
	type: 'reset',
	options: {
		resetWithOption: true,
	}
});

How to call reset with payload

reset(
payload?: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
),
options?: OptionsParams,
): void

Note

If a payload is sent, then the initial state and the current state will be replaced with the payload. Affects the following states:

state new state
state.current payload
state.previous old state.current
state.initial payload
previousInitial old state.initial
state.changes null since state.initial equals state.current
reset state with payload example code
// sets current state and initial state equal to payload
dispatch.cta.reset({
	search: 'dispatch.cta.reset',
	isFuzzy: true,
	count: 10,
});
reset state with payload and option example code
// sets current state and initial state equal to payload
dispatch.cta.reset(
	{
		search: 'dispatch.cta.reset with options',
		isFuzzy: true,
		count: 10,
	},
	{
		resetInitialWithOption: true,
	}
);
reset state with payload as callback example code
// sets current state and initial state equal to payload
dispatch.cta.reset(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// prevent reset from triggering
			return;
		}
	
		// sets current state and initial state equal to payload
		return {
			search: 'dispatch.cta.reset with callback',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count,
		}
	}
);
reset state with payload as callback and option example code
// sets current state and initial state equal to payload
dispatch.cta.reset(
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	(ctaPayloadCallbackParameter) => {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// prevent reset from triggering
			return;
		}
		
		return {
			search: 'dispatch.cta.reset with callback with options',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count + 1,
		}
	},
	{
		resetCallbackWithOption: true,
	}
);
Using dispatcher function instead of dispatch.cta.reset with payload

type ResetCTAProps<Initial extends CTAInitial,> = {
type: 'reset'
payload?: undefined
options?: OptionsParams
} | {
type: 'reset'
payload?: Initial | (
( ctaPayloadCallbackParameter: CTAPayloadCallbackParameter<Initial> ) => Initial | undefined
)
options?: OptionsParams
};

dispatch({
	type: 'reset',
	payload: {
		search: 'dispatch reset',
		isFuzzy: true,
		count: 10,
	}
});

dispatch({
	type: 'reset',
	payload: {
		search: 'dispatch reset with option',
		isFuzzy: true,
		count: 10,
	},
	options: {
		resetInitialWithOption: true,
	}
});

dispatch({
	type: 'reset',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | undefined)} returning `undefined` prevents action from triggering.
	 */
	payload(ctaPayloadCallbackParameter) {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// prevent reset from triggering
			return;
		}

		// sets current state and initial state equal to payload
		return {
			search: 'dispatch.cta.reset with callback',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count,
		}
	},
});

dispatch({
	type: 'reset',
	/**
	 * @param {CTAPayloadCallbackParameter<CTAInitial>} ctaPayloadCallbackParameter
	 * @returns {(CTAInitial | void)} returning `undefined` prevents action from triggering.
	 */
	payload(ctaPayloadCallbackParameter) {
		if (ctaPayloadCallbackParameter.current.count > 10) {
			// prevent reset from triggering
			return;
		}

		return {
			search: 'dispatch.cta.reset with callback',
			isFuzzy: true,
			count: ctaPayloadCallbackParameter.current.count + 1,
		}
	},
	options: {
		resetCallbackWithOption: true,
	}
});

How to augment reset

reset?: ( ctaState: CTAStateParam<Initial>, payload?: Initial ) => Initial | undefined

augment reset example code
import {useEffect} from 'react';
import {useCTA, CTAStateParam,} from 'react-hook-use-cta'

const initial = {
	search: 'initial',
	isFuzzy: false,
	count: 0,
}

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial,
		actions: {
			/**
			 * @param {CTAStateParam<typeof initial>} ctaStateParam
			 * @param {typeof initial=} payload - optional
			 * @returns {(typeof initial | void)} returning `void` prevents action from triggering.
			 */
			reset(ctaStateParam, payload,) {
				const {
					current,
					options,
				} = ctaStateParam;
				
				// You must handle `payload` that is `undefined`
				if (!payload) {
					// this will set current = initial
					return ctaStateParam.initial;
				}
				
				let {
					count,
				} = payload;
				
				if (!Number.isSafeInteger(count)) {
					// prevent reset from triggering
					return;
				}
				
				// set count to current.count if allowNegativeCount is falsey and count is less than 0
				if (count < 0 && !options?.allowNegativeCount) {
					count = current.count;
				}

				return {
					...payload,
					count,
				};
			}
		}
	});

	useEffect(
		() => {
			dispatch.cta.reset(
				{
					search: 'reset',
					isFuzzy: true,
					count: -1,
				},
				{
					allowNegativeCount: true,
				}
			);
		},
		[
			dispatch,
		]
	);
	
	// will render `-1`
	return state.count;
}

Custom Actions

export type UseCTAParameterActionsCustomRecord<Initial extends CTAInitial,> = {
[customAction: string | number]: (
(
ctaParam: CustomCTAStateParam<Initial>,
// Needs to be `any` in order to take any type.
payload?: any // eslint-disable-line
) => CustomCTAReturnType<Initial>
)
};

Note

When the available actions aren't enough, you can define your own specialized custom actions using action behaviors.

Important

All custom action callbacks receive a CustomCTAStateParam as their first parameter with the following properties.

initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null
options?: Readonly<OptionsParams>
updateAction( result: Partial<Initial>, options?: ActionTypeConstructParam<Initial>['options'] ): UpdateActionType<Initial>
replaceAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ReplaceActionType<Initial>
replaceInitialAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ReplaceInitialActionType<Initial>
resetAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ResetActionType<Initial>
The second parameter depends on what you want sent as a payload

Warning

Augmented existing call to actions become the default behavior when using them in custom actions. To use non-augmented behavior, provide {useDefault: true} option as the second parameter.

options?: { useDefault: boolean }

How to define and call custom action as update behavior

Important

All custom actions behave as an update when returning a Partial<CTAInitial>.

Defining custom update action
import { useEffect, } from 'react';
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: {
			count: 0,
		},
		actions: {
			addToCount(ctaParam, value: number) {
				return {
					count: ctaParam.previous.count + value,
				}
			},
			incrementCount(ctaParam) {
				return {
					count: ctaParam.current.count + 1,
				}
			},
		}
	});

	useEffect(
		() => {
			dispatch.cta.incrementCount();
			dispatch.cta.addToCount(3)
		},
		[]
	);

	// renders `4`
	return state.count;
}
Defining custom update action using updateAction behavior
import { useEffect, } from 'react';
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: {
			count: 0,
			search: '',
		},
		actions: {
			update(ctaParam, payload) {
				const {
					count = ctaParam.current.count,
				} = payload;
				return {
					...payload,
					count: count + 1
				};
			},
			multiplyCount(ctaParam, value: number) {
				return ctaParam.updateAction(
					{
						count: ctaParam.current.count * value
					},
					{
						// don't update using augmented behavior.
						useDefault: true,
					}
				)
			},
		}
	});

	useEffect(
		() => {
			dispatch.cta.update('search', 'update');
			dispatch.cta.multiplyCount(7)
		},
		[]
	);

	// renders `7`
	return state.count;
}

How to define and call custom action as updateInitial behavior

Defining custom updateInitial action using updateInitialAction
import { useEffect, } from 'react';
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: {
			count: 0,
			search: '',
			isFuzzy: false,
		},
		actions: {
			sourceSync(ctaParam,) {
				return ctaParam.updateInitialAction(
					{
						count: 13,
						search: 'sourceSync',
						isFuzzy: true,
					}
				)
			},
		}
	});

	useEffect(
		() => {
			dispatch.cta.sourceSync();
		},
		[]
	);

	return <>
		{/* renders `13` */}
		<div>{dispatch.state.initial.count}</div>
		{/* renders `sourceSync` */}
		<div>{dispatch.state.initial.search}</div>
		{/* renders `true` */}
		<div>{dispatch.state.initial.isFuzzy}</div>
		{/* renders `0` */}
		<div>{state.count}</div>
		{/* renders `` */}
		<div>{state.search}</div>
		{/* renders `false` */}
		<div>{state.isFuzzy}</div>
	</>;
}

How to define and call custom action as reset behavior

Defining custom reset action using resetAction
import { useEffect, } from 'react';
import { useCTA, } from 'react-hook-use-cta'

function View() {
	const [
		state,
		dispatch,
	] = useCTA({
		initial: {
			count: 0,
			search: '',
			isFuzzy: false,
		},
		actions: {
			sync(ctaParam,) {
				return ctaParam.resetAction(
					{
						count: 13,
						search: 'sync',
						isFuzzy: true,
					}
				)
			},
		}
	});

	useEffect(
		() => {
			dispatch.cta.sync();
		},
		[]
	);

	return <>
		{/* renders `null` */}
		<div>{dispatch.state.changes}</div>
		{/* renders `13` */}
		<div>{dispatch.state.initial.count}</div>
		{/* renders `sync` */}
		<div>{dispatch.state.initial.search}</div>
		{/* renders `true` */}
		<div>{dispatch.state.initial.isFuzzy}</div>
		{/* renders `13` */}
		<div>{state.count}</div>
		{/* renders `sync` */}
		<div>{state.search}</div>
		{/* renders `true` */}
		<div>{state.isFuzzy}</div>
	</>;
}

createCTAContext

Playground

Note

Combines useCTA with React createContext and useContext. Accepts the same parameters as useCTA:

export type UseCTAParameter<
Initial extends CTAInitial,
Actions extends UseCTAParameterActionsRecordProp<Initial> | undefined,
> = {
actions?: Actions
initial: Initial
onInit?: ( ( initial: Initial ) => Initial )
};

Create a file for exporting, example globalContext.ts
import { createCTAContext, } from 'react-hook-use-cta'

export const GlobalContext = createCTAContext({
	initial: {
		search: 'initial',
		isFuzzy: false,
		count: 0,
	},
});

Returns an object the following key/value:

CTAProvider

Note

Provider to wrap the app or component for context. It accepts props:

CTAProvider example code
import GlobalContext from './globalContext';
import { GlobalCountView, } from './GlobalCountView'
import { GlobalCountButton, } from './GlobalCountButton'

const appInitial = {
	search: 'app',
	isFuzzy: true,
	count: 11,
}

export function App() {
	return <GlobalContext.CTAProvider initial={appInitial}>
		<GlobalCountButton/>
		<GlobalCountView/>
	</GlobalContext.CTAProvider>;
}

useCTAStateContext

Note

Hook that returns the current state

useCTAStateContext example code
import { GlobalContext, } from './globalContext';

const {
	useCTAStateContext
} = GlobalContext;

export function GlobalCountView() {
	const globalState = useCTAStateContext();
	return <div>
		{globalState.count}
	</div>;
}

useCTADispatchContext

Note

Hook that returns cta dispatcher. Returns null if called outside CTAProvider

useCTADispatchContext example code
import { useCallback, } from 'react';
const {
	useCTADispatchContext
} = GlobalContext;

export function GlobalCountButton() {
	const globalDispatch = useCTADispatchContext();
	const onClick = useCallback(
		() => {
			globalDispatch.cta.update((state) => {
				return {
					count: state.current.count + 1,
				}
			})
		},
		[
			globalDispatch
		]
	)
	return <button {...{
		onClick,
	}}>
		Update count:
	</button>;
}

returnActionsType

export function returnActionsType<
Initial extends CTAInitial,
Actions extends UseCTAParameterActionsRecordProp<Initial>,
>( initial: Initial, actions: Actions, ) {
return actions;
}

Note

In case you need to define actions parameter from a variable, this function can help infer actions type

returnActionsType example code
import { returnActionsType, } from 'react-hook-use-cta';

const initial = {
	search: 'initial',
	isFuzzy: false,
	count: 0,
};
const actions = returnActionsType(
	initial,
	{
		setSearch(state, search: string) {
			return {
				search
			}
		}
	}
);

Typescript exports

export type { CTAInitial, } from './types/CTAInitial';
export type { UseCTAParameter, } from './types/UseCTAParameter';
export type { UseCTAReturnType, } from './types/UseCTAReturnType';
export type { UseCTAReturnTypeDispatch, } from './types/UseCTAReturnTypeDispatch';
export type { CTAPayloadCallbackParameter, } from './types/UseCTAReturnTypeDispatch';
export type { CustomCTAStateParam, } from './types/CustomCTAStateParam';
export type { CTAStateParam, } from './types/CTAStateParam';
export type { CustomCTAReturnType, } from './types/CustomCTAReturnType';

export type { CTAInitial, }

export type CTAInitial = Record<string | number, unknown>;

export type { UseCTAParameter, }

export type UseCTAParameter<
Initial extends CTAInitial,
Actions extends UseCTAParameterActionsRecordProp<Initial> | undefined,
> = {
actions?: Actions
initial: Initial
onInit?: ( ( initial: Initial ) => Initial )
};

export type { UseCTAReturnType, }

export type UseCTAReturnType<
Initial extends CTAInitial,
Actions = undefined,
> = [
Initial,
UseCTAReturnTypeDispatch<Initial, Actions>,
];

export type { UseCTAReturnTypeDispatch, }

export type UseCTAReturnTypeDispatch<
Initial extends CTAInitial,
Actions = undefined,
> = DispatchCTA<Initial, Actions> & {
readonly cta: UseCTAReturnTypeDispatchCTA<Initial, Actions>
readonly state: UseCTAReturnTypeDispatchState<Initial>
};

export type { CTAPayloadCallbackParameter, }

export type CTAPayloadCallbackParameter<Initial extends CTAInitial,> = Readonly<
Pick<
CustomCTAStateParam<Initial>,
'changes' | 'current' | 'initial' | 'previous'
>
>;

export type { CustomCTAStateParam, }

export type CustomCTAStateParam<Initial extends CTAInitial,> = Readonly<{
initial: Readonly<Initial>
current: Readonly<Initial>
previous: Readonly<Initial>
changes: Readonly<Partial<Initial>> | null
options?: Readonly<OptionsParams>
updateAction( result: Partial<Initial>, options?: ActionTypeConstructParam<Initial>['options'] ): UpdateActionType<Initial>
replaceAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ReplaceActionType<Initial>
replaceInitialAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ReplaceInitialActionType<Initial>
resetAction( result: Initial, options?: ActionTypeConstructParam<Initial>['options'] ): ResetActionType<Initial>
}>;

export type { CTAStateParam, }

export type CTAStateParam<Initial extends CTAInitial,> = Readonly<
Pick<
CustomCTAStateParam<Initial>,
'changes' | 'current' | 'initial' | 'previous' | 'options'
>
>;