Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: Suggestion: Simplify TagInput Component State Management using useControllableState #95

Open
2 tasks done
oyerindedaniel opened this issue Sep 15, 2024 · 0 comments

Comments

@oyerindedaniel
Copy link

oyerindedaniel commented Sep 15, 2024

Feature description

Currently, the TagInput component requires external state management to track tags via tags and setTags props. This approach, while functional, adds boilerplate code and makes the component less flexible for users who might not need to manage state externally.

To simplify state management and make the component more user-friendly, I suggest introducing useControllableState, a hook that checks whether the component is controlled or uncontrolled and allows it to handle state internally when necessary.

Current Behavior:

In the current version, users need to declare a state outside the TagInput component, like so:

const [tags, setTags] = React.useState<Tag[]>((form?.getValues?.('skills') as Tag[]) || []);
The TagInput then accepts tags and setTags as props:
<TagInput
  {...field}
  enableAutocomplete
  autocompleteOptions={Skills}
  limitToAutoComplete
  placeholder="Enter skills"
  tags={tags}
  setTags={setTags}
/>

This approach works, but it requires the user to manage the state externally, even in scenarios where the state could be handled internally by the component itself.

Additional Context

Proposed Solution:

By introducing the useControllableState hook, we can make the TagInput component capable of handling its internal state without requiring external state management. This will reduce the need to declare external states and simplify the component's usage.

Tags and SetTags are still passed, but they are manually mutated in the tag form field directly by RHF (React Hook Form).

<TagInput
  {...field}
  enableAutocomplete
  autocompleteOptions={Skills}
  limitToAutoComplete
  placeholder="Enter skills"
  tags={(field.value ?? []) as Tag[]}
 setTags={field.onChange}
/>

This hooks are used in most component library

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

function useCallbackRef<T extends (...args: any[]) => any>(
  callback: T | undefined,
  deps: React.DependencyList = []
) {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  });

  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useCallback(((...args) => callbackRef.current?.(...args)) as T, deps);
}

/**
 * Given a prop value and state value, the useControllableProp hook is used to determine whether a component is controlled or uncontrolled, and also returns the computed value.
 *
 * @see Docs https://chakra-ui.com/docs/hooks/use-controllable#usecontrollableprop
 */
function useControllableProp<T>(prop: T | undefined, state: T) {
  const controlled = typeof prop !== 'undefined';
  const value = controlled ? prop : state;
  return useMemo<[boolean, T]>(() => [controlled, value], [controlled, value]);
}

interface UseControllableStateProps<T> {
  value?: T;
  defaultValue?: T | (() => T);
  onChange?: (value: T) => void;
  shouldUpdate?: (prev: T, next: T) => boolean;
}

/**
 * The useControllableState hook returns the state and function that updates the state, just like React.useState does.
 *
 * @see Docs https://chakra-ui.com/docs/hooks/use-controllable#usecontrollablestate
 */
function useControllableState<T>(props: UseControllableStateProps<T>) {
  const {
    value: valueProp,
    defaultValue,
    onChange,
    shouldUpdate = (prev, next) => prev !== next
  } = props;

  const onChangeProp = useCallbackRef(onChange);
  const shouldUpdateProp = useCallbackRef(shouldUpdate);

  const [uncontrolledState, setUncontrolledState] = useState(defaultValue as T);
  const controlled = valueProp !== undefined;
  const value = controlled ? valueProp : uncontrolledState;

  const setValue = useCallbackRef(
    (next: React.SetStateAction<T>) => {
      const setter = next as (prevState?: T) => T;
      const nextValue = typeof next === 'function' ? setter(value) : next;

      if (!shouldUpdateProp(value, nextValue)) {
        return;
      }

      if (!controlled) {
        setUncontrolledState(nextValue);
      }

      onChangeProp(nextValue);
    },
    [controlled, onChangeProp, value, shouldUpdateProp]
  );

  return [value, setValue] as [T, React.Dispatch<React.SetStateAction<T>>];
}
const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
  const {
    tags
    setTags
    // other props
  } = props;

  const [tags, setTags] = useControllableState({
    value: tags,
    onChange: setTags,
  });

  const handleAddTag = (newTag) => {
    setTags((prevTags) => [...prevTags, newTag]);
  };

  // rest of the logic
});

Before submitting

  • I've made research efforts and searched the documentation
  • I've searched for existing issues and PRs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant