How to give rendering control to users with prop getters

If you’re an egghead.io subscriber, you can also learn about this pattern with
these two lessons: Use Prop Collections with Render
Props

and Use Prop Getters with Render
Props

Since I released downshift 🏎 a few
weeks ago. Of all things, I think the most common question I’ve gotten has been
about the “prop getters.” As far as I know,
downshift is the first library to
implement this pattern, so I thought I’d explain why it’s useful and how to
implement it. If you’re unfamiliar with downshift, please read
the intro post before you continue.
Don’t worry, I’ll wait…

dog wagging the tail while waiting

So, to recap from what you read, prop getters are one piece to the puzzle to
let you hand rendering over to the users of your components
(a great idea). I
got the idea from Jared Forsyth one day at
an airport. You can only really use it with the
render prop pattern.
It’s basically a function which will return props when called and people must
apply those props to the right element to hook together all the relevant
elements to make the overarching component.
Hopefully that’s clear 😀

To talk about this, we’ll actually use a different component I wrote recently
that uses this pattern called
react-toggled.

React toggled logo

It’s pretty small, so I’m just going to paste all of it here for you
(see the syntax highlighted file here):

import {Component} from 'react'
import PropTypes from 'prop-types'

const callAll =
  (...fns) =>
  (...args) =>
    fns.forEach(fn => fn && fn(...args))

class Toggle extends Component {
  static propTypes = {
    defaultOn: PropTypes.bool,
    on: PropTypes.bool,
    onToggle: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.array]).isRequired,
  }
  static defaultProps = {
    defaultOn: false,
    onToggle: () => {},
  }
  state = {
    on: this.getOn({on: this.props.defaultOn}),
  }

  getOn(state = this.state) {
    return this.isOnControlled() ? this.props.on : state.on
  }

  isOnControlled() {
    return this.props.on !== undefined
  }

  getTogglerProps = (props = {}) => ({
    'aria-controls': 'target',
    'aria-expanded': Boolean(this.getOn()),
    ...props,
    onClick: callAll(props.onClick, this.toggle),
  })

  getTogglerStateAndHelpers() {
    return {
      on: this.getOn(),
      getTogglerProps: this.getTogglerProps,
      setOn: this.setOn,
      setOff: this.setOff,
      toggle: this.toggle,
    }
  }

  setOnState = (state = !this.getOn()) => {
    if (this.isOnControlled()) {
      this.props.onToggle(state, this.getTogglerStateAndHelpers())
    } else {
      this.setState({on: state}, () => {
        this.props.onToggle(this.getOn(), this.getTogglerStateAndHelpers())
      })
    }
  }

  setOn = this.setOnState.bind(this, true)
  setOff = this.setOnState.bind(this, false)
  toggle = this.setOnState.bind(this, undefined)

  render() {
    const renderProp = unwrapArray(this.props.children)
    return renderProp(this.getTogglerStateAndHelpers())
  }
}

/**
 * Takes an argument and if it's an array, returns the first item in the array
 * otherwise returns the argument
 * @param {*} arg the maybe-array
 * @return {*} the arg or it's first item
 */

function unwrapArray(arg) {
  return Array.isArray(arg) ? arg[0] : arg
}

export default Toggle

You’ll notice that this.props.children is unwrapped, this is for preact
compatibility.

And here’s how you could use react-toggled:

<Toggle>
  {({on, getTogglerProps}) => (
    <div>
      <button
        {...getTogglerProps({
          onClick() {
            alert('you clicked!')
          },
        })}
      >
        Toggle me
      </button>
      <div>{on ? 'Toggled On' : 'Toggled Off'}</div>
    </div>
  )}
</Toggle>

There are a few neat things about this component I may talk about in a future
post, but for now, let’s focus on the getTogglerProps function (that’s the
prop getter).

The cool thing about this pattern is that it allows users to render whatever
they want. So your components take care of the hard and generic part (the logic
of the component) and the user can take care of the easy and less-generic part:
what to show and how it’s styled given the state of the component.

So if users want the <div> to appear above the <button> or to not appear at
all, then the user can simply do that without having to look up any docs for
props or anything. This is pretty powerful!

With that said,
the biggest question I get from folks about “prop getters”
is:

Why are you using a function to get props? Why not just pass a regular object
to my render callback and let me spread that instead of having to call a
function?

What people are saying is they’d prefer to do:
<button {...togglerProps} {...myOwnProps} /> rather than
<button {...getTogglerProps(myOwnProps)} />. I can understand why folks might
prefer that. It feels like you have more control that way. However, we’re
actually doing something useful with this function and the props that you
provide…

For this component, we care about the onClick prop you apply to your
<button>. We need to call this.toggle. But what if you (as a user of the
component) also wanted to have a handler for onClick? You might try to write
it like this: <button onClick={this.handleClick} {...togglerProps} />. But
you’d find that togglerPropsoverrides your custom onClick handler, so you
could switch it to: <button {...togglerProps} onClick={this.handleClick} />
and now you have the opposite problem! Your custom onClick is overriding the
onClick from togglerProps, so react-toggled isn’t working at all.

With that context, let’s see how we avoid this problem by using a function.
Check out the implementation of getTogglerProps:

getTogglerProps = (props = {}) => ({
  'aria-controls': 'target',
  'aria-expanded': Boolean(this.getOn()),
  ...props,
  onClick: callAll(props.onClick, this.toggle),
})

You’ll notice that the onClick prop is assigned to
callAll(props.onClick, this.toggle). The callAll function is pretty simple:

const callAll =
  (...fns) =>
  (...args) =>
    fns.forEach(fn => fn && fn(...args))

It does what it says. Calls all the functions it’s given, if they exist. In our
case, both of our onClick handlers will be called as we need. (See
the transpiled version
if you’re less accustomed to arrow functions).


To summarize, prop getters are one of the patterns that enable you to hand
rendering responsibility to the user of your components (a really awesome idea).
You can only really implement it with the render prop pattern (in our case we
use the children prop, but you could use a render prop if you prefer).

Here are a few projects that implement the prop getters pattern:

  • downshift
    🏎 – Primitive for building
    simple, flexible, WAI-ARIA compliant enhanced input React components
  • react-toggled – Component to
    build simple, flexible, and accessible toggle components
  • dub-step
    🕺 – Step through an index with
    style
  • react-stepper-primitive
    React primitives for a “stepper” component.

I hope to see more folks doing stuff like this in the future! Good luck to you
all! 👍




Source link

مدونة تقنية تركز على نصائح التدوين ، وتحسين محركات البحث ، ووسائل التواصل الاجتماعي ، وأدوات الهاتف المحمول ، ونصائح الكمبيوتر ، وأدلة إرشادية ونصائح عامة ونصائح