hsuanyi-chou / shadcn-ui-expansions

More components built on top of shadcn-ui.
https://shadcnui-expansions.typeart.cc/
MIT License
1.1k stars 51 forks source link

Reactive data to option #19

Closed mr-scrpt closed 10 months ago

mr-scrpt commented 10 months ago

My data for the list comes from the backend and initially the options are an empty array and when the response comes from the backend the Multiple Selector component was not updating the list, I added this code and now the data has become reactive

useEffect(() => {
      setOptions(transToGroupOption(arrayOptions, groupBy));
    }, [arrayOptions, groupBy]);
hsuanyi-chou commented 10 months ago

Did you use onSearch prop ? I think this prop fits your needs.

Async Search with Debounce

there's a similar code currently.

    useEffect(() => {
      const exec = async () => {
        if (!debouncedSearchTerm || !onSearch) return;
        setIsLoading(true);
        const res = await onSearch?.(debouncedSearchTerm);
        setOptions(transToGroupOption(res, groupBy));   // <----- here
        setIsLoading(false);
      };

      void exec();
    }, [debouncedSearchTerm]);
hsuanyi-chou commented 10 months ago

I've added a prop triggerSearchOnFocus which might fit most use cases and refactored the onSearch executed condition.

Here is the usage.

It seems like there might be a potential issue with using !debouncedSearchTerm inside the useEffect that could lead to unexpected situations.

mr-scrpt commented 10 months ago

Hi. I don't understand what a search has to do with my problem. Here code:

          <FormField
            control={form.control}
            name="optionList"
            render={({ field }) => {
              return (
                <FormItem>
                  <FormLabel>Option</FormLabel>
                  <FormControl>
                    <MultipleSelector
                      value={field.value}
                      onChange={field.onChange}
                      options={optionList}
                      placeholder="Select option to category"
                      emptyIndicator={
                        <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
                          no results found.
                        </p>
                      }
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              );
            }}
          />

This is where I pass on the list of options options={optionList} The problem is that this list is initially empty, and after some time, when the request to the server is executed, this list gets the required data - this happens asynchronously But the MultipleSelector component does not respond to props changes options

const [options, setOptions] = React.useState<GroupOption>(
  transToGroupOption(arrayOptions, groupBy),
);

Since it moves it to the state during initialization and works with the stateit already Therefore, when you change the props, you need to update the stack with new data.

useEffect(() => {
      setOptions(transToGroupOption(arrayOptions, groupBy));
    }, [arrayOptions, groupBy]);
hsuanyi-chou commented 10 months ago

You have to put your request in onSearch prop and return a GroupOtion[] value.
Just return the api res in onSearch prop and the component will trigger setOption.

<MultipleSelector
        // start here-----------------
        onSearch={async (value) => {
          const res = await mockSearch(value); // Your API.
          return res;  // return a value which  type is `GroupOtions`.
        }}
        triggerSearchOnFocus  // You may need the prop to get init options when `onFocus`
        // end here-----------------
        placeholder="trying to search 'a' to get more options..."
        loadingIndicator={<p>loading...</p>}
        emptyIndicator={<p>no results found.</p>}
      />
mr-scrpt commented 10 months ago

I still can't understand what search has to do with it - I'm not searching for anything. My request to api is executed through server action in zustand, then it is passed to child component in props where MultipleSelector is located. The initiating state for the options is an empty array, when the request is executed, the zustand storage will be mutated, and the necessary options will appear there for me to select from.

hsuanyi-chou commented 10 months ago

I got it. You are right about the useEffect snippet. I didn't do this useEffect because I was afraid of the performance issue of multiple re-renders.

I will add a JSON.stringify to avoid the same value re-render and to refactor original options to defaultOptions.

    useEffect(() => {
      /** If `onSearch` is provided, do not trigge options updated. */
      if (!arrayOptions || onSearch) {
        return;
      }
      const newOption = transToGroupOption(arrayOptions || [], groupBy);
      if (JSON.stringify(newOption) !== JSON.stringify(options)) {
        setOptions(newOption);
      }
    }, [arrayDefaultOptions, arrayOptions, groupBy, onSearch, options]);

here is the usage