question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[RFC] Change terra-select to Data API from Children API

See original GitHub issue

Feature Request

Description

This is a companion RFC to https://github.com/cerner/terra-core/issues/2492.

We have been discussing using a data API instead of (or in addition to) the children API for terra-form-select.

Related

Children API (current)

This is the current API for all terra-form-select variants. Simply pass a list of Select.Option components as children to the Select.

Predefined Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PredefinedDataChildrenSelect() {
  // nothing special, hardcode a list of options here
  // NOTE: this is really rare in real world applications
  return (
    <Select placeholder="Select a color">
      <Select.Option value="blue" display="Blue" />
      <Select.Option value="green" display="Green" />
      <Select.Option value="purple" display="Purple" />
      <Select.Option value="red" display="Red" />
      <Select.Option value="violet" display="Violet" />
    </Select>
  );
}
Prop Driven Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PropDrivenDataChildrenSelect({ options }) {
  // map over list of options, transform shape if necessary
  // for this example, we use the shape { id: String, text: String },
  // instead of the expected { value: String, display: String }
  return (
    <Select placeholder="Select a color">
      {options.map(option => (
        <Select.Option
          key={option.id}
          value={option.id}
          display={option.text}
        />
      ))}
    </Select>
  );
}
Predefined Grouped Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PredefinedGroupedDataChildrenSelect() {
  // nothing special, hardcode a list of grouped options here
  // NOTE: this is really rare in real world applications
  return (
    <Select placeholder="Select a color">
      <Select.OptGroup label="Shade of blue">
        <Select.Option value="blue" display="Blue" />
        <Select.Option value="cyan" display="Cyan" />
        <Select.Option value="teal" display="Teal" />
        <Select.Option value="azul" display="Azul" />
        <Select.Option value="aqua" display="Aqua" />
      </Select.OptGroup>
      <Select.OptGroup label="Shades of green">
        <Select.Option value="green" display="Green" />
        <Select.Option value="forest" display="Forest Green" />
        <Select.Option value="dark" display="Dark Green" />
        <Select.Option value="neon" display="Neon Green" />
      </Select.OptGroup>
    </Select>
  );
}
Prop Driven Grouped Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PropDrivenGroupedDataChildrenSelect({ groups }) {
  // map over list of groups
  // for each group, map over list of options
  // no need to transform data for this example
  return (
    <Select placeholder="Select a color">
      {groups.map(group => (
        <Select.OptGroup label={group.label} key={group.label}>
          {group.options.map(option => (
            <Select.Option
              key={option.value}
              value={option.value}
              display={option.display}
            />
          ))}
        </Select.OptGroup>
      ))}
    </Select>
  );
}
All Children API Examples Usage & Render
import React from 'react';
import { render } from 'react-dom';

function ChildrenApiExamples() {
  // this list could come from anywhere, or be any shape really
  // to illustrate data transformations (a common use case),
  // we use the shape { id: String, text: String }
  // instead of the expected { value: String, display: String }
  const options = [
    { id: 'blue', text: 'Blue' },
    { id: 'green', text: 'Green' },
    { id: 'purple', text: 'Purple' },
    { id: 'red', text: 'Red' },
    { id: 'violet', text: 'Violet' },
  ];

  const groupedOptions = [
    {
      label: 'Shades of blue',
      options: [
        { value: 'blue', display: 'Blue' },
        { value: 'cyan', display: 'Cyan' },
        { value: 'teal', display: 'Teal' },
        { value: 'azul', display: 'Azul' },
        { value: 'aqua', display: 'Aqua' },
      ],
    },
    {
      label: 'Shades of green',
      options: [
        { value: 'green', display: 'Green' },
        { value: 'forest', display: 'Forest Green' },
        { value: 'dark', display: 'Dark Green' },
        { value: 'neon', display: 'Neon Green' },
      ],
    },
  ];

  return (
    <div>
      <PredefinedDataChildrenSelect />
      <PropDrivenDataChildrenSelect options={options} />
      <PredefinedGroupedDataChildrenSelect />
      <PropDrivenGroupedDataChildrenSelect groups={groupedOptions} />
    </div>
  );
}

render(<ChildrenApiExamples />, document.getElementById('root'));

Pros

  • Matches the native <select> API fairly closely, with a few changes
    • <Select.Option> uses display prop instead of children for innerText
  • For hardcoded data, it’s really easy to use
  • Allows flexibility in using <OptGroup>s or <Option>s manually

Cons

  • Doesn’t handle dynamic data very well (more on this later)
  • In real world examples, you need to manually map over data, applying transforms if necessary, and know that you need to render <Select.Option> and <Select.OptGroup> components here
  • larger surface area in userland code === more surface area for bugs

Data API

Predefined Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PredefinedDataSelect() {
  // nothing special, hardcode a list of options here
  // can either define the data as a local, or just inline it as done below
  // NOTE: this is really rare in real world applications
  return (
    <Select
      placeholder="Select a color"
      data={[
        { value: 'blue', display: 'Blue' },
        { value: 'green', display: 'Green' },
        { value: 'purple', display: 'Purple' },
        { value: 'red', display: 'Red' },
        { value: 'violet', display: 'Violet' }
      ]}
    />
  );
}
Prop Driven Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PropDrivenDataSelect({ options }) {
  // Thought: maybe we allow the consumer to pass an optional dataTransform
  // callback which allows the select to handle when the data items aren't
  // of the expected shape, like so:
  function dataTransform(option) {
    return { value: option.id, display: option.text };
  }

  // delegate the display of options to the Select itself
  return (
    <Select
      placeholder="Select a color"
      data={options}
      dataTransform={dataTransform}
    />
  );
}
Predefined Grouped Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PredefinedGroupedDataSelect() {
  // nothing special, hardcode a list of grouped options here
  // NOTE: this is really rare in real world applications
  const groupedOptions = [
    {
      label: 'Shades of blue',
      options: [
        { value: 'blue', display: 'Blue' },
        { value: 'cyan', display: 'Cyan' },
        { value: 'teal', display: 'Teal' },
        { value: 'azul', display: 'Azul' },
        { value: 'aqua', display: 'Aqua' },
      ],
    },
    {
      label: 'Shades of green',
      options: [
        { value: 'green', display: 'Green' },
        { value: 'forest', display: 'Forest Green' },
        { value: 'dark', display: 'Dark Green' },
        { value: 'neon', display: 'Neon Green' },
      ],
    },
  ];

  // Note, this isn't necessarily a good idea here since we need to explore
  // a way to tell the select that the options are in fact grouped, another
  // prop is used here as a boolean, for example
  return (
    <Select
      placeholder="Select a color"
      data={groupedOptions}
      groupedOptions
    />
  );
}
Prop Driven Grouped Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';

function PropDrivenGroupedDataSelect({ groupedOptions }) {
  // Note, this isn't necessarily a good idea here since we need to explore
  // a way to tell the select that the options are in fact grouped, another
  // prop is used here as a boolean, for example
  return (
    <Select
      placeholder="Select a color"
      data={groupedOptions}
      groupedOptions
    />
  );
}
All Data API Examples Usage & Render
import React from 'react';
import { render } from 'react-dom';

function DataApiExample() {
  // this list could come from anywhere, or be any shape really
  // to illustrate data transformations (a common use case),
  // we use the shape { id: String, text: String }
  // instead of the expected { value: String, display: String }
  const options = [
    { id: 'blue', text: 'Blue' },
    { id: 'green', text: 'Green' },
    { id: 'purple', text: 'Purple' },
    { id: 'red', text: 'Red' },
    { id: 'violet', text: 'Violet' },
  ];

  const groupedOptions = [
    {
      label: 'Shades of blue',
      options: [
        { value: 'blue', display: 'Blue' },
        { value: 'cyan', display: 'Cyan' },
        { value: 'teal', display: 'Teal' },
        { value: 'azul', display: 'Azul' },
        { value: 'aqua', display: 'Aqua' },
      ],
    },
    {
      label: 'Shades of green',
      options: [
        { value: 'green', display: 'Green' },
        { value: 'forest', display: 'Forest Green' },
        { value: 'dark', display: 'Dark Green' },
        { value: 'neon', display: 'Neon Green' },
      ],
    },
  ];

  return (
    <div>
      <PredefinedDataSelect />
      <PropDrivenDataSelect options={options} />
      <PredefinedGroupedDataSelect />
      <PropDrivenGroupedDataSelect groupedOptions={groupedOptions} />
    </div>
  );
}

render(<DataApiExample />, document.getElementById('root'));

Pros

  • Simpler userland code === less surface area for bugs
  • API is more focused and opinionated
    • Allows for easier access for optimizations on terra’s end (e.g. windowing and async data)
  • Delegates presentational responsibility of options to the <Select> itself
    • OCS doesn’t allow you to roll custom options (yet lol)
  • Still flexible enough to allow for grouped options, especially if we allow a first class prop for dataTransform and the interface shape is strictly defined and well documented

Cons

  • Deviates from the native <select> API
  • API is more focused and opinionated
  • For hardcoded data, it’s not as easy to use. But again, this is really rare in real world code.

Extra Information

Asynchronous Data

The issues with the current Select’s implementation become clear when you are using a multi-select variant. If you make a list of selections, they are simply held in state by value (id).

This is problematic because the selected options display as <Tag> components which use the value (id, non-presentational text) to find the child with the correct value and get the display from and use that as the child text (Related, see: https://github.com/cerner/terra-core/issues/2369)

Could we simply cache the entire selected object? Sure. This would allow us to not have to search through the children for a child with a specified value to get the presentational text. Actually, that’s what I’m proposing here. We could do the following:

Example code
this.state {
  selected: []
};

function addSelection(option) {
  // this is just for demonstrating the expected shape
  const newSelection = {
    value: option.props.value,
    display: option.props.display,
  };

  this.setState(state => ({
    selected: [...state.selected, newSelection];
  }))
}

function removeLastSelected() {
  this.setState(state => ({
    selected: state.selected.slice(0, state.selected.length - 1),
  }));
}

function removeSelectedByValue(value) {
  this.setState(state => ({
    selected: state.selected.filter(selection => selection.value !== value),
  }));
}

This is actually a slight tangent, but is still related to the current children implementation since we search through the children to find the display of the option with the matching value.

Allowing both APIs at the same time

We could do this as well, but I’m cautious here since I think we should err on the side of simpler implementation and an opinionated API. It is also possible to limit the children API to only the default variant since it shouldn’t contain dynamic data or long lists, however, I question the overall value of doing this.


Do you think this is a feature you would use? Could it simplify your userland code? Do you “Thanks, I hate it”? Comments welcome!

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:1
  • Comments:19 (16 by maintainers)

github_iconTop GitHub Comments

2reactions
StephenEssercommented, Jul 9, 2019

For asynchronous operations I think users would favor the data driven API, but I would like to see an analysis of how and why the data API reduces the overall amount of code and complexity within the Select as is alluded to with this RFC.

Many of the arguments presented for the data API can be resolved in the same way, or in a similar way, with a children API. The arguments themselves actually appear to add more code and complexity. This RFC itself presents the potential of additional props for data transforms and managing the opt groups. dataTransform and groupedOptions. The usage of groups in general appears ambiguous and undecided with the data API whereas this is a non-issue with the children API.

In real world examples, you need to manually map over data, applying transforms if necessary, and know that you need to render <Select.Option> and <Select.OptGroup> components here

The data transform and mapping is an important discussion point. The data returned from a service will likely not match the API necessary for the select options. So the data will need to be massaged into the correct format in both the data API and the children API. The mapping that is repeatedly marked as a pain point with the existing children API is also present in the data driven API. In the scenario where the dataTransform isn’t a part of the API the users will still need to map and transform the data returned from a service into correctly formatted options. This would be a requirement for either API, the data driven API does not solve the mapping. It only moves the logic into the additional dataTransform (hypothetically this gets added) prop which would make the prop type for options unknown and unstructured. The user could pass any shape and transform the data appropriately for the options. This allows a high level of freedom for the user but also means the shape of the options is unknown and cannot be reliably interpreted.

Doesn’t handle dynamic data very well (more on this later)

So, you’re correct. The multi-select does need to maintain an internal selection list, and it does. However, it only keeps the values, instead of both the value and display. This is an internal implementation detail of the terra-select that is lacking. I think this will illustrate the situation a little better:

Caching the selected options for dynamic loading would resolve the behavior of the displays reverting back to their base values for both the children and data API. This is not inherently an issue with the children API, this is an issue that is present in both the data API and the children API. A cache of selection options would resolve the issue in either API.

larger surface area in userland code === more surface area for bugs

I don’t think enough data has been collected to support this. The data mapping / transforms will be required for both API proposals. The only real difference is where and what objects the data is mapped into. <Select.Options> vs {}.

How would you expect to handle asynchronous data? As in the following example:

Probably the exact same way planned with the data API. Caching.

How would you expect to handle windowing (for performance gains)?

Probably the exact same way planned with the data API.

Is there a use case where we allow or use custom <Foo.Option>s?

Many teams have reached out with this question wanting to implement their own custom options. Although we are not directly supporting this, many teams are wanting this functionality. Our decision thus far has been not to allow custom options, largely for accessibility reason, but the want and need are present.

Is it not strange to have to know that you need to do either:

No, this is not strange. This is the behavior many React components follow and would be documented on the UI site. It’s also a very similar behavior you’d expect with native html. Most engineers that have prior experience with a Select would likely be familiar with the native select, which uses a children API, or Select2 which also uses the children API.

Fundamentally the children API is just an object. This object could theoretically be treated the same as the options prop within the data API. I haven’t seen any use-case presented that the data API resolves that cannot also be resolved in a near identical way with the children API.

There is also a really high tax that hasn’t been addressed with moving to the data API. This change will impact every consumer of the Select to transform their APIs. The changes required by splitting the packages are intended to be small initially, this API change will be a lot more impactful.

Many of the pros and cons mentioned above apply to both APIs and would be resolved in a similar way.

All of that said. I think we should continue to discuss and consider the data API, fundamentally I think the decision should come down to “which API is easier to use for the 90% use-case”.

2reactions
yuderekyucommented, Jul 5, 2019

Is there a use case where we allow or use custom <Foo.Option>s?

We have some discussion about that here - https://github.com/cerner/terra-core/issues/2254. Seems like we should not open up options to be custom. Instead, if the use case arises, a new component should be created to handle it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to update data from child component to parent ...
I have two components app.js and posts.js. My app.js component import React, { createContext, useState } from 'react' ...
Read more >
Passing Data Between React Components — Parent ...
Step 2: Pass the state and the callback function as props to all children inside the Provider Component. The provider is the boss...
Read more >
rfcs/0002-new-version-of-context.md at main · reactjs/rfcs
React will detect a new object reference and trigger a change. Only one provider type per consumer. The proposed API only allows for...
Read more >
Vue 3 Composition API Tutorial #8 - Child Components
In this series you'll learn everything you need to know to get started with Vue 3 & The Composition API. This series is...
Read more >
Using React's Context API with Typescript | Pluralsight
In this guide, you'll learn how to use strongly typed React contexts with TypeScript so you don't have to manually pass down props...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found