UNPKG

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