Using SelectField component for related data inside scaffold form not applying selection or displaying on refetch

Hi,

I want to enhance the generated RW Scaffolds (that are basically forms) and add ability to select an option for related data. ex. if User had a Favorite Movie assigned to the account that is Many to one relationship, but instead of displaying user.favoriteMoveId that RW would generate I want to display a list of all movies with default option pointing to currently selected one if it’s present.

If not I want the field to be a SelectField type with all possible ‘Movies’ as options.

The other way to phrase this question would be: how do I run multiple queries inside a cell and wait for all of them to finish before rendering the component?

I want to know what is the proper way (in RW) to do it:

  1. should I create a new query inside EditXXXCell.tsx to fetch all the “movies” so they can be displayed?
  2. should I create a new cell that will display “movies” as Selection field with options?
  3. should the data for select field be fetched inside the form?
  4. Maybe it would be better to create a new component for that purpose?

I managed to do it with option 1 but only partially see below (please note that code is adapted to the example mentioned above, I am not making movie db):

This would be an additional GraphQL query in EditUserCell

export const QUERY_MOVIES = gql`
  query FindAllMovies {
    movies {
      id
      title
    }
  }
`;

And I will run it on Success:

export const Success = ({ exercise }: CellSuccessProps<EditUserById>) => {
  const findMovies = useQuery(QUERY_MOVIES, {
    onCompleted: (data) => {
      toast.success('Movies fetched');
      moviesData = data.movies;
    },
  });
...
}

The SelectField in the Form component:

<SelectField
          name="favoriteMovie"
          defaultValue={props.user?.favoriteMovie?.id}
          className="rw-input"
          errorClassName="rw-input rw-input-error"
          placeholder="Select a Favorite Movie"
        >
          {props.favoriteMovie?.map((favMovie) => {
            return (
              <option
                key={favMovie?.id}
                value={favMovie?.id}
              >
                {favMovie?.title}
              </option>
            );
          })}
        </SelectField>

Now there are 2 problems with this:

  1. When I click edit from the scaffold data is fetched and displayed but item selected is not the item user has assigned (using ID as value).
  2. When edit page is refreshed all data is lost, although the toast shows that data was fetched it’s not assigned to the options and SelectField is empty.

After re-checking the docs it seems I should create a Cell that will fetch data and render the component to fix no. 2. No. 1 should be fixable with SelectField component but…

when trying to wrap SelectField into a concrete instance for this example (Movie List) I receive an error from react-hook-form:

Cannot read properties of null (reading 'formState')

  return (
    <SelectField
      name="movieSelect"
      defaultValue={'id-of-the-movie'}
    >
      {movies.map((movie: Movie) => {
        
        return (
          <option
            key={movie.id}
            value={movie.id}
          >
            {movie.title}
          </option>
        );
      })}
    </SelectField>
  );
};

Should I wrap storybook in some FormProvider ? Why cannot I use the RW components in Storybook on their own?

So my issue was fixed by RTFM and paying attention to details from docs.

I used 2 queries inside single QUERY string receiving data for single row in ModelA and all data from ModelB. These are exposed as variables (there is a note in tutorial and docs I think, can’t link this at the moment). Then it was a matter of setting the defaultValue on SelectField - this might have been trickier that it should as I am using CUIDs for ID fields… not sure… I had to cast ID .toString() to finally get it working. Added answer here as well:

I think it would be a good thing to add some documentation (reference?) on how to use the built in SelectField as there is part of docs touching on the topic.

@Sebastian Getting the same error while running Test cases with the same example can you help me on the same use case, please?

Sure I wanted to post what I found about using select fields, managing many-to-many relationships with custom component (ex. React Select) I just do not have the time and “document” or “manual” for that is more than 1300 lines long.

But you need to do few things - in the Query of Edit…Cell add secondary query ex.:

Query = `
namedQuery: model {
id
}
secondaryModels {
 id
otherFields
}
`;

then add the secondaryModels to success component and pass it to the form as options/choices for Select component.
for New component you need to first create a New[model]Cell to fetch data inside it and pass data to New[Model] component and then again to the form from New component.

Many to many relationships are a bit more tricky and there are gotchas here and there. I will really try to publish something that I do (over 1400 lines long document with code) but I need to adapt the code as it’s not public.

I am also very curious how the community is doing that and what are the best practices.

For the component itself - you should wrap it inside a Controller component provided by Redwood but I am not sure I am doing it all correctly (but it works!).

Thanks for hint

This is how I created a filed for react-select. This method worked (and it does work with validation without ‘Field’ suffix in the name.

import Select from 'react-select';
import { GetOptionLabel, GetOptionValue } from 'react-select';

import {
  Control,
  Controller,
  FieldValues,
  RegisterOptions,
} from '@redwoodjs/forms';

const ReactSelectField = ({
  name,
  options = [],
  defaultValue = [],
  isMulti = false,
  getOptionValue = (option) => option?.id,
  getOptionLabel = (option) => option?.title,
  className: additionalClassName,
  validation,
  control,
  ...rest
}: {
  name: string;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  options: [any] | [];
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  defaultValue: [any] | [] | object;
  isMulti?: boolean;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getOptionValue?: GetOptionValue<any>;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getOptionLabel?: GetOptionLabel<any>;
  className?: string;
  validation?: Omit<
    RegisterOptions,
    'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
  >;
  control?: Control<FieldValues, object>;
}) => {
  return (
    <Controller
      name={name}
      defaultValue={defaultValue}
      key={name}
      control={control}
      rules={validation}
      render={({ field }) => {
        return (
          <Select
            {...field}
            className={additionalClassName}
            options={options}
            defaultValue={defaultValue}
            getOptionValue={getOptionValue}
            getOptionLabel={getOptionLabel}
            isMulti={isMulti}
            onChange={(val) => {
              field?.onChange(val);
            }}
            {...rest}
          />
        );
      }}
    />
  );
};

export default ReactSelectField;

I do wonder if there is a better way to do it. If anyone has better examples please let me know!