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

Reusable React Components #5

Open
coryhouse opened this issue Apr 18, 2018 · 0 comments
Open

Reusable React Components #5

coryhouse opened this issue Apr 18, 2018 · 0 comments

Comments

@coryhouse
Copy link
Owner

coryhouse commented Apr 18, 2018

Reusable React Components

My Course - Building Reusable React Components

Lessons Learned Building a React Component Library in Typescript blog post

Consider multiple abstraction levels - May make sense to implement simple standalone components and then compose them in opinionated ways. Consider offering both low and high level abstractions, as inspired by the visualization libraries post above.

Rigid vs Flexible tradeoffs:

Rigid benefits:

  • Enforces consistency
  • Encourages teams to reach out so we can systematize customizations by adding props
  • Fewer props = simpler, but can lead to proliferation of props if we keep adding ad-hoc props for styling.
  • Less duplication risk
  • Can programmatically enforce best-practices such as accessibility
  • Building each component takes longer, since we have to carefully try to cover all scenarios

Flexible benefits:

  • Favors developer autonomy and initial dev speed. Less need to wait for a new component library release to get what you need.
  • Encourages use. If it's not flexible enough, people just won't use, or will create ugly hacks based on implementation details.
  • The APIs and styles need not be perfect. Can be more freewheeling.
  • Developers may rely upon classnames to customize, making DOM changes in the component library risky

Broadly, choose an abstraction level, or perhaps multiple.

Choosing a CSS abstraction level via customization-vs-configuration-in-evolving-design-systems
image

Questions

  • Have you created any reusable components yet?
  • How are you handling reused components within a single project?
  • Open to considering monorepo?
  • Specific components in mind?

Storybook Tips

  • Skip HTTP for mock APIs. Instead, develop mock API that return Promises.
  • Use setTimeout to fake 1 second delay so preloaders show
  • Use the same mock data as you use for tests and driving the app's mock API.
  • Document all the potential states listed below.
  • Place stories alongside the component so they're easier to find, open
  • Consider knobs, info plugins

Picking a Third Party Library

Headless component libraries

If you have a specific design system from your designer, it might be easier and better solution to go with headless components that come unstyled than to adapt a fully featured library's components to your needs. These tools solve the most important accessibility challenges while remaining totally agnostic when it comes to cosmetics and styles:

Reakit - Focused on a11y
Chakra UI - Okay, it has some styles, but listing here since it basically has some lightly styled defaults
Reach UI
Headless UI
Radix UI
react-aria
MUI Base
Lexical - headless editor from Meta, alternatives: Slate, Tiptop
Tanstack Table, TanStack Virtual (similar to react-virtualized, react-window)
Downshift
Riakit / AriaKit
TailwindUI
cmdk
FloatingUI
React dnd-kit
Formik, React Hook Form
https://react-hot-toast.com/
Many more options and context in this slidedeck

Lexical - Rich text editor (WYSIWYG)

How to choose between the libraries above:

  • Library ecosystem
  • Accessibility
  • Have a corp style guide? Need to match existing design?
  • What sorts of components do you envision yourself needing?
  • Styling approach? Need to consume styles outside of React?
  • Documentation
  • Browser support
  • Open issues
  • Consider grabbing a few off the shelf like a datepicker, then build the rest.

Potential Component States

A handy checklist to consider. Typically, it's useful to have a dedicated storybook story for each applicable state below.

  1. Disabled
  2. Initial state (before any interaction on the screen - sometimes just renders empty)
  3. Responsive design
  4. Validation error
  5. No data
  6. Focused
  7. Hover
  8. Lack of permissions
  9. Data changed / pristine / not persisted
  10. Loading state (spinner / skeleton / no render / progress bar...)
  11. Slow connection (consider displaying a "Keep waiting?" message)
  12. Loading timed out - Can also consider a fallback state that displays cached data and a button to retry. Example: A todo's list component fails to load, so you display the todos that were stored in localStorage. The user also sees the message: "The latest data failed to load. Click here to try again."
  13. Action timed out - Example: A save failed. Keep users data on the screen and display a message "Sorry, save failed. Click save to try again."
  14. API call error
  15. Offline - One could be offline, but still be able to write if the component writes to localStorage. Though it's helpful to tell the user their data is being temporarily stored locally until a network connection exists.
  16. Read only
  17. Using a screenreader
  18. Consider and test for different permutations of props

Related tweets here and here

Core Decisions

Dev Environment

Documentation

Sandbox / playground / live editing

https://github.com/nihgwu/react-runner
https://github.com/FormidableLabs/react-live
https://github.com/codesandbox/sandpack
Tweet

Design

States - When a user is interacting with a component, what are the different states the entire component or its parts can go through? Common states include:

  • Hover - The mouse is over the component. Keep in mind this state will never be seen in mobile.
  • Focus - The component has cursor focus, so typing will affect this component.
  • Active/Pressed - Usually only visible briefly e.g while the mouse button is pressed.
  • Selected - Mainly applies to lists or toggle-able elements.
  • Disabled - The user can't interact with it currently even though it usually is an interactive component.
  • During dragging - We haven't figured out a general approach here yet.
  • Error states - Do errors apply to this component? Does it need to catch the user's attention for a critical or patient safety issue?

Content - What is the range of content that a component can take?

  • Text - Is it only text or can it be anything?
  • Size - How small or big can it get?
  • Truncation - If we set a size limit, how do we truncate and indicate to the user that there's content they can't see?

Surroundings - What are the different surroundings a component is likely to be used in and do they affect the design? Common relevant contexts include:

  • Darker background - Do the component colors need to change?
  • Limited space (or very large space) - how does the component accommodate space limitations
  • Mobile - No hover states or cursor in addition to smaller size (not considering much now, but should start)
  • Co-location - Is this component's relationship to another piece of the UI especially important? If so, what are the variations of that other component to consider?

Animation - When changing states or content, what do the transitions look like?

  • Layout - Layout transitions like height and width are especially tricky and can impact the work needed to implement.
  • Speed - Preference for fast or slow? Often this is based on size.

More Design Concerns

  • Use useId to avoid id collisions. Useful for associating label and input in reusable components.
  • Make impossible states impossible. Group related props into an object.
  • Declare contain CSS to improve rendering performance
  • Atomic Design
    • Atoms
    • Molecules
    • Organisms
  • Keep booleans positive
  • Extend HTML base element's props and pass them to the root - And consider the tradeoffs in different approaches to doing so
  • Honor native API. Accept native HTML props and pass them down to the underlying element. Avoid creating a new API that doesn't honor the plain HTML element.
  • Prefer false defaults to shorten JSX
  • Avoid the boolean trap - Avoid using booleans that may conflict. Instead, accept an "enum" for mutually exclusive options. Example, accept a buttonType prop with a list of potential string values like primary, secondary, etc, rather than isPrimary, isSecondary.
  • Watch out for bool props - might be a sign you need an enum. Might also be a sign two separate components would be preferable (much like funcs)
  • Create a BaseProvider for things like themes, i11n, LTR
  • Internationalization (i11n) - Nice example from Uber's base UI
  • Support "addons" props for TextInput, etc. addons: [{ element: <MyElement/>, position: POSITION.AFTER_INPUT}] or simply belowAddons: <MyElement>.
  • Different approaches: Accept child components or strings for things like headers on tabs or styled <H1> - <H5> components. Use isValidElement to determine if string or element passed.
  • Selecting an audience
  • Be consistent. Example: If you're prepending boolean property names with is or has in one place, do so consistently.
  • Allow caller to declare the top-level DOM element that will be rendered - For example, use or
    instead of
  • Export an enum for a finite set of props so the consumer can import the enum along with the component and use it, and use the enum in propType declaration for oneOf
  • Wrap HTML primitives?
    • Don't rename HTML attributes. Never override HTML attributes in your components. A great example is the element's type attribute. It can be submit (the default), button or reset. However, a lot of developers tend to re-purpose this prop name to mean the visual type of button (primary, cta and so on). By repurposing this prop, you have to add another override to set the actual type attribute, and it only leads to confusion, doubt and sore users.
    • Whenever your component is passing a prop in your implementation, like a class name or an onClick handler, make sure the external consumer can do the same thing. How:
  • Use props.children for more flexibility, and consider creating named "slots" if a single child isn't sufficient. Names slots are useful when the component allows the user to place arbitrary content in multiple spots (such as a card component with different named sections, or a pagelayout component with named header, body and footer slots.
  • Consider accepting an "as" prop to declare the top level element type
  • Folder structure
  • Declare propTypes and be specific. Start required. Loosen as needed.
  • Set useful defaults so users can pass fewer props. Plus, you need not check within the component
  • Apply custom props?
  • Defaults?
  • Accessibility
  • Server rendering
  • Config object vs. primitives
  • The orchestration pattern
  • Compound components using context. Also a simple tabs example
  • Only allow certain prop types - This forbids using certain child elements by validating each child element tag
  • More here - The 10 component commandments

Mobile

Mobile friendly component options:

  1. Responsive design, so no mobile-specific features. Example: BottomSlidePanel
  2. Built-in media query - Examples: IconButton, Accordion, Table. Advantage: Easy. Disadvantage: Not configurable. Compromise: Include mobileMaxWidth or isMobile prop.
  3. isMobile prop - isMobile prop - Consumer must set it. Downside: Extra work that people will likely forget. And prop name doesn’t specify what this does. Gotta check the docs. And if the consumer wants only some of the mobile features, they can’t specify. It’s all or nothing. Advantage: Could use this same convention for any components, if we can accept the caveats.
  4. mobileMaxWidth prop - mobileMaxWidth - Advantage: Slightly better than previous option since consumer doesn’t have to read the width to determine when the mobile features should kick in. Disadvantage: All the same as previous.
  5. Well-named prop that describes the specific behavior. Advantage: Can use certain features. Disadvantage: User must wire it up and read the screen width. Example: Avatar. Table density. 
  6. Separate mobile component - Advantage: Can lazy load, or not use at all if not relevant. Avoids bloating bundle or slowing loads. And can use on desktop too if relevant (for better or worse). But more work to weave, and many won’t bother. Example: https://material-ui.com/components/steppers/#mobile-stepper

Styling

  • See Styling list

Testing

  • Framework
  • Assertions
  • Helpers
  • Unit
  • Interaction
  • Structural
  • Style
  • When to run
  • Where to place tests
  • Continuous Integration
  • More here

Development decisions, tips, and workflow

  • Will this be maintained by a single, centralized team?
  • Open or closed source? Inner source?
  • Keeping UX and UI in sync
  • The rule of three
    • Consider standalone npm packages at first, then deprecate when pulled into central lib
    • Use nwb or better yet your own solution with more opinions
    • Consider a naming scheme or centralized documentation for standalone components to aid discovery
  • Testing changes before publish
    • Link
    • Pack
    • Beta
    • Lerna bootstrap
  • Documentation
    • Use as dev environment
    • Generate or build by hand
    • Use a boilerplate, doc tool, or build your own?

Deployment

  • npm package structure
  • Automated build
    • Exclude React/React-DOM from the build
    • CommonJS build
    • ES build
    • UMD build
  • Dependency declarations     
  • Hosting
  • Versioning - honor semver
  • Import approach
  • Declaring package files
  • Output formats
  • Pre-publish testing
  • Build process

Validate children - only allow certain types of children

const allowedChildren = ["string", "span", "em", "b", "i", "strong"];

  function isSupportedElement(child: React.ReactElement) {
    return (
      allowedChildren.some((c) => c === child.type) || ReactIs.isFragment(child)
    );
  }

  // Only certain child elements are accepted. Recursively check child elements to assure all elements are supported.
  function validateChildren(children: React.ReactNode) {
    return React.Children.map(children, (child) => {
      if (!React.isValidElement(child)) return child;

      const elementChild: React.ReactElement = child;
      if (child.props.children) validateChildren(elementChild.props.children);

      if (!isSupportedElement(elementChild)) {
        throw new Error(
          `Children of type ${
            child.type
          } aren't permitted. Only the following child elements are allowed in Inline Alert: ${allowedChildren.join(
            ", "
          )}`
        );
      }
      return elementChild;
    });
  }

  // For performance, only run during development
  if (inDev) {
    validateChildren(children);
  }

More solid tips in this blog post

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