Skip to content

Commit

Permalink
fix: refactor controlled and uncontrolled state workings
Browse files Browse the repository at this point in the history
  • Loading branch information
chrispulsinelli-okta committed Dec 1, 2023
1 parent 7cacefc commit c3f3f54
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 285 deletions.
100 changes: 47 additions & 53 deletions packages/odyssey-react-mui/src/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ import {
UseAutocompleteProps,
AutocompleteValue,
} from "@mui/material";
import { memo, useCallback, useMemo } from "react";
import { memo, useCallback, useMemo, useRef } from "react";

import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import {
ComponentControlledState,
useInputValues,
getControlState,
} from "./inputUtils";

export type AutocompleteProps<
OptionType,
Expand All @@ -31,7 +35,6 @@ export type AutocompleteProps<
> = {
/**
* The default value. Use when the component is not controlled.
* @default props.multiple ? [] : null
*/
defaultValue?: UseAutocompleteProps<
OptionType,
Expand Down Expand Up @@ -189,6 +192,45 @@ const Autocomplete = <
getIsOptionEqualToValue,
testId,
}: AutocompleteProps<OptionType, HasMultipleChoices, IsCustomValueAllowed>) => {
const controlledStateRef = useRef(
getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
);
const defaultValueProp = useMemo<
| AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>
| undefined
>(() => {
if (hasMultipleChoices) {
if (value === undefined) {
return defaultValue;
}
return [] as AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>;
}
return value === undefined ? defaultValue : undefined;
}, [defaultValue, hasMultipleChoices, value]);

const valueProps = useInputValues({
defaultValue: defaultValueProp,
value: value,
controlState: controlledStateRef.current,
});

const inputValueProp = useMemo(() => {
if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
return { inputValue };
}
return undefined;
}, [inputValue]);

const renderInput = useCallback(
({ InputLabelProps, InputProps, ...params }) => (
<Field
Expand Down Expand Up @@ -223,52 +265,6 @@ const Autocomplete = <
),
[errorMessage, hint, isOptional, label, nameOverride]
);

const defaultValuesProp = useMemo<
| AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>
| undefined
>(() => {
if (hasMultipleChoices) {
return defaultValue === undefined
? ([] as AutocompleteValue<
OptionType,
HasMultipleChoices,
undefined,
IsCustomValueAllowed
>)
: defaultValue;
}
return defaultValue ?? undefined;
}, [defaultValue, hasMultipleChoices]);

const [localValue, setLocalValue] = useControlledState({
controlledValue: value,
uncontrolledValue: defaultValuesProp,
});

const valueProps = useMemo(() => {
if (localValue === undefined) {
return { defaultValue: defaultValuesProp };
}
return { value: localValue };
}, [defaultValuesProp, localValue]);

const [localInputValue, setLocalInputValue] = useControlledState({
controlledValue: inputValue,
uncontrolledValue: undefined,
});

const inputValueProp = useMemo(() => {
return {
inputValue: inputValue === undefined ? undefined : localInputValue,
};
}, [inputValue, localInputValue]);

const onChange = useCallback<
NonNullable<
UseAutocompleteProps<
Expand All @@ -280,10 +276,9 @@ const Autocomplete = <
>
>(
(event, value, reason, details) => {
setLocalValue(value);
onChangeProp?.(event, value, reason, details);
},
[onChangeProp, setLocalValue]
[onChangeProp]
);

const onInputChange = useCallback<
Expand All @@ -297,10 +292,9 @@ const Autocomplete = <
>
>(
(event, value, reason) => {
setLocalInputValue(value);
onInputChangeProp?.(event, value, reason);
},
[onInputChangeProp, setLocalInputValue]
[onInputChangeProp]
);

return (
Expand Down
26 changes: 13 additions & 13 deletions packages/odyssey-react-mui/src/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import { useTranslation } from "react-i18next";
import { memo, useCallback, useMemo } from "react";
import { memo, useCallback, useMemo, useRef } from "react";
import {
Checkbox as MuiCheckbox,
CheckboxProps as MuiCheckboxProps,
Expand All @@ -22,7 +22,7 @@ import {
import { FieldComponentProps } from "./FieldComponentProps";
import { Typography } from "./Typography";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import { ComponentControlledState, getControlState } from "./inputUtils";
import { CheckedFieldProps } from "./FormCheckedProps";

export const checkboxValidityValues = ["valid", "invalid", "inherit"] as const;
Expand Down Expand Up @@ -90,17 +90,18 @@ const Checkbox = ({
value,
}: CheckboxProps) => {
const { t } = useTranslation();
const [isLocalChecked, setIsLocalChecked] = useControlledState({
controlledValue: isChecked,
uncontrolledValue: isDefaultChecked,
});

const controlledStateRef = useRef(
getControlState({
controlledValue: isChecked,
uncontrolledValue: isDefaultChecked,
})
);
const inputValues = useMemo(() => {
if (isLocalChecked === undefined) {
return { defaultChecked: isDefaultChecked };
if (controlledStateRef.current === ComponentControlledState.CONTROLLED) {
return { checked: isChecked };
}
return { checked: isLocalChecked };
}, [isDefaultChecked, isLocalChecked]);
return { defaultChecked: isDefaultChecked };
}, [isDefaultChecked, isChecked]);

const label = useMemo(() => {
return (
Expand All @@ -121,10 +122,9 @@ const Checkbox = ({

const onChange = useCallback<NonNullable<MuiCheckboxProps["onChange"]>>(
(event, checked) => {
setIsLocalChecked(checked);
onChangeProp?.(event, checked);
},
[onChangeProp, setIsLocalChecked]
[onChangeProp]
);

return (
Expand Down
34 changes: 14 additions & 20 deletions packages/odyssey-react-mui/src/NativeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import React, {
memo,
useCallback,
useMemo,
useRef,
} from "react";
import {
Select as MuiSelect,
Expand All @@ -24,7 +25,7 @@ import {
import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import { getControlState, useInputValues } from "./inputUtils";
import { ForwardRefWithType } from "./@types/react-augment";

export type NativeSelectOption = {
Expand Down Expand Up @@ -103,37 +104,30 @@ const NativeSelect: ForwardRefWithType = forwardRef(
onChange: onChangeProp,
onFocus,
testId,
value: valueProp,
value,
children,
}: NativeSelectProps<Value, HasMultipleChoices>,
ref?: React.Ref<ReactElement>
) => {
const [localValue, setLocalValue] = useControlledState({
controlledValue: valueProp,
uncontrolledValue: defaultValue,
const controlledStateRef = useRef(
getControlState({
controlledValue: value,
uncontrolledValue: defaultValue,
})
);
const inputValues = useInputValues({
defaultValue,
value,
controlState: controlledStateRef.current,
});

const inputValues = useMemo(() => {
if (localValue === undefined) {
return { defaultValue };
}
return { value: localValue };
}, [defaultValue, localValue]);

const onChange = useCallback<
NonNullable<MuiSelectProps<Value>["onChange"]>
>(
(event, child) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
const { options } = event.target as HTMLSelectElement;
const selectedOptions = [...options]
.filter((option) => option.selected)
.map((selectedOption) => selectedOption.value);
setLocalValue(selectedOptions as Value);
onChangeProp?.(event, child);
},
[onChangeProp, setLocalValue]
[onChangeProp]
);

const hasMultipleChoices = useMemo(
Expand Down
28 changes: 14 additions & 14 deletions packages/odyssey-react-mui/src/PasswordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
forwardRef,
memo,
useCallback,
useMemo,
useRef,
useState,
} from "react";

Expand All @@ -26,7 +26,7 @@ import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import type { SeleniumProps } from "./SeleniumProps";
import { useTranslation } from "react-i18next";
import { useControlledState } from "./useControlledState";
import { getControlState, useInputValues } from "./inputUtils";

export type PasswordFieldProps = {
/**
Expand Down Expand Up @@ -94,7 +94,7 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
onBlur,
placeholder,
testId,
value: valueProp,
value,
},
ref
) => {
Expand All @@ -107,25 +107,25 @@ const PasswordField = forwardRef<HTMLInputElement, PasswordFieldProps>(
);
}, []);

const [localValue, setLocalValue] = useControlledState({
controlledValue: valueProp,
uncontrolledValue: defaultValue,
const controlledStateRef = useRef(
getControlState({
controlledValue: value,
uncontrolledValue: defaultValue,
})
);
const inputValues = useInputValues({
defaultValue,
value,
controlState: controlledStateRef.current,
});
const inputValues = useMemo(() => {
if (localValue === undefined) {
return { defaultValue };
}
return { value: localValue };
}, [defaultValue, localValue]);

const onChange = useCallback<
ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>
>(
(event) => {
setLocalValue(event.target.value);
onChangeProp?.(event);
},
[onChangeProp, setLocalValue]
[onChangeProp]
);

const renderFieldComponent = useCallback(
Expand Down
24 changes: 10 additions & 14 deletions packages/odyssey-react-mui/src/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import {
RadioGroup as MuiRadioGroup,
type RadioGroupProps as MuiRadioGroupProps,
} from "@mui/material";
import { memo, ReactElement, useCallback, useMemo } from "react";
import { memo, ReactElement, useCallback, useRef } from "react";

import { Radio, RadioProps } from "./Radio";
import { Field } from "./Field";
import { FieldComponentProps } from "./FieldComponentProps";
import type { SeleniumProps } from "./SeleniumProps";
import { useControlledState } from "./useControlledState";
import { getControlState, useInputValues } from "./inputUtils";

export type RadioGroupProps = {
/**
Expand Down Expand Up @@ -62,24 +62,20 @@ const RadioGroup = ({
testId,
value,
}: RadioGroupProps) => {
const [localValue, setLocalValue] = useControlledState({
controlledValue: value,
uncontrolledValue: defaultValue,
const controlledStateRef = useRef(
getControlState({ controlledValue: value, uncontrolledValue: defaultValue })
);
const inputValues = useInputValues({
defaultValue,
value,
controlState: controlledStateRef.current,
});

const inputValues = useMemo(() => {
if (localValue === undefined) {
return { defaultValue };
}
return { value: localValue };
}, [localValue, defaultValue]);

const onChange = useCallback<NonNullable<MuiRadioGroupProps["onChange"]>>(
(event, value) => {
setLocalValue(value);
onChangeProp?.(event, value);
},
[onChangeProp, setLocalValue]
[onChangeProp]
);
const renderFieldComponent = useCallback(
({ ariaDescribedBy, errorMessageElementId, id, labelElementId }) => (
Expand Down
Loading

0 comments on commit c3f3f54

Please sign in to comment.