UNPKG

6.26 kBJavaScriptView Raw
1'use client';
2
3import _extends from "@babel/runtime/helpers/esm/extends";
4import * as React from 'react';
5function areEqual(a, b) {
6 return a === b;
7}
8const EMPTY_OBJECT = {};
9const NOOP = () => {};
10
11/**
12 * Gets the current state augmented with controlled values from the outside.
13 * If a state item has a corresponding controlled value, it will be used instead of the internal state.
14 */
15function getControlledState(internalState, controlledProps) {
16 const augmentedState = _extends({}, internalState);
17 Object.keys(controlledProps).forEach(key => {
18 if (controlledProps[key] !== undefined) {
19 augmentedState[key] = controlledProps[key];
20 }
21 });
22 return augmentedState;
23}
24/**
25 * Defines an effect that compares the next state with the previous state and calls
26 * the `onStateChange` callback if the state has changed.
27 * The comparison is done based on the `stateComparers` parameter.
28 */
29function useStateChangeDetection(parameters) {
30 const {
31 nextState,
32 initialState,
33 stateComparers,
34 onStateChange,
35 controlledProps,
36 lastActionRef
37 } = parameters;
38 const internalPreviousStateRef = React.useRef(initialState);
39 React.useEffect(() => {
40 if (lastActionRef.current === null) {
41 // Detect changes only if an action has been dispatched.
42 return;
43 }
44 const previousState = getControlledState(internalPreviousStateRef.current, controlledProps);
45 Object.keys(nextState).forEach(key => {
46 var _stateComparers$key;
47 // go through all state keys and compare them with the previous state
48 const stateComparer = (_stateComparers$key = stateComparers[key]) != null ? _stateComparers$key : areEqual;
49 const nextStateItem = nextState[key];
50 const previousStateItem = previousState[key];
51 if (previousStateItem == null && nextStateItem != null || previousStateItem != null && nextStateItem == null || previousStateItem != null && nextStateItem != null && !stateComparer(nextStateItem, previousStateItem)) {
52 var _event, _type;
53 onStateChange == null || onStateChange((_event = lastActionRef.current.event) != null ? _event : null, key, nextStateItem, (_type = lastActionRef.current.type) != null ? _type : '', nextState);
54 }
55 });
56 internalPreviousStateRef.current = nextState;
57 lastActionRef.current = null;
58 }, [internalPreviousStateRef, nextState, lastActionRef, onStateChange, stateComparers, controlledProps]);
59}
60
61/**
62 * The alternative to `React.useReducer` that lets you control the state from the outside.
63 *
64 * It can be used in an uncontrolled mode, similar to `React.useReducer`, or in a controlled mode, when the state is controlled by the props.
65 * It also supports partially controlled state, when some state items are controlled and some are not.
66 *
67 * The controlled state items are provided via the `controlledProps` parameter.
68 * When a reducer action is dispatched, the internal state is updated with the new values.
69 * A change event (`onStateChange`) is then triggered (for each changed state item) if the new state is different from the previous state.
70 * This event can be used to update the controlled values.
71 *
72 * The comparison of the previous and next states is done using the `stateComparers` parameter.
73 * If a state item has a corresponding comparer, it will be used to determine if the state has changed.
74 * This is useful when the state item is an object and you want to compare only a subset of its properties or if it's an array and you want to compare its contents.
75 *
76 * An additional feature is the `actionContext` parameter. It allows you to add additional properties to every action object,
77 * similarly to how React context is implicitly available to every component.
78 *
79 * @template State - The type of the state calculated by the reducer.
80 * @template Action - The type of the actions that can be dispatched.
81 * @template ActionContext - The type of the additional properties that will be added to every action object.
82 *
83 * @ignore - internal hook.
84 */
85export function useControllableReducer(parameters) {
86 const lastActionRef = React.useRef(null);
87 const {
88 reducer,
89 initialState,
90 controlledProps = EMPTY_OBJECT,
91 stateComparers = EMPTY_OBJECT,
92 onStateChange = NOOP,
93 actionContext,
94 componentName = ''
95 } = parameters;
96 const controlledPropsRef = React.useRef(controlledProps);
97 if (process.env.NODE_ENV !== 'production') {
98 // eslint-disable-next-line react-hooks/rules-of-hooks
99 React.useEffect(() => {
100 Object.keys(controlledProps).forEach(key => {
101 if (controlledPropsRef.current[key] !== undefined && controlledProps[key] === undefined) {
102 console.error(`useControllableReducer: ${componentName ? `The ${componentName} component` : 'A component'} is changing a controlled prop to be uncontrolled: ${key}`);
103 }
104 if (controlledPropsRef.current[key] === undefined && controlledProps[key] !== undefined) {
105 console.error(`useControllableReducer: ${componentName ? `The ${componentName} component` : 'A component'} is changing an uncontrolled prop to be controlled: ${key}`);
106 }
107 });
108 }, [controlledProps, componentName]);
109 }
110
111 // The reducer that is passed to React.useReducer is wrapped with a function that augments the state with controlled values.
112 const reducerWithControlledState = React.useCallback((state, action) => {
113 lastActionRef.current = action;
114 const controlledState = getControlledState(state, controlledProps);
115 const newState = reducer(controlledState, action);
116 return newState;
117 }, [controlledProps, reducer]);
118 const [nextState, dispatch] = React.useReducer(reducerWithControlledState, initialState);
119
120 // The action that is passed to dispatch is augmented with the actionContext.
121 const dispatchWithContext = React.useCallback(action => {
122 dispatch(_extends({}, action, {
123 context: actionContext
124 }));
125 }, [actionContext]);
126 useStateChangeDetection({
127 nextState,
128 initialState,
129 stateComparers: stateComparers != null ? stateComparers : EMPTY_OBJECT,
130 onStateChange: onStateChange != null ? onStateChange : NOOP,
131 controlledProps,
132 lastActionRef
133 });
134 return [getControlledState(nextState, controlledProps), dispatchWithContext];
135}
\No newline at end of file