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

RFD: Moving to a hook api 🎣 #871

Closed
alexreardon opened this issue Oct 26, 2018 · 33 comments
Closed

RFD: Moving to a hook api 🎣 #871

alexreardon opened this issue Oct 26, 2018 · 33 comments

Comments

@alexreardon
Copy link
Collaborator

React hooks have recently been announced.

Would we benefit from moving from a render props api to a hook based api for react-beautiful-dnd? If so, what would it even look like?

I have not thought very much about this, but I wanted to open it for discussion

@YurkaninRyan
Copy link

Oh wow this is an interesting question. I think a hook based api would have to come along with a render prop based api. Otherwise, you wouldn't be able to consume the hooks in non-function components.

I'm not 100% sure what a useDraggable or useDroppable would even look like.

I would assume that it would use useEffect and useContext to do its work?

@gnapse
Copy link

gnapse commented Oct 26, 2018

Maybe something like this?

function Card({ itemId, index }) {
  const draggable = useDraggable({ draggableId: itemId, index });
  return (
    <div {...draggable.draggableProps} ref={draggable.innerRef}>
      <h1 {...draggable.dragHandleProps}>Drag me by the title</h1>
      <p>...</p>
    </div>
  );
}

I mean, this is just syntactically how I imagine it. Not sure how to make it workable, not familiar with the inner workings of this lib (would love to but haven't had time).

@YurkaninRyan
Copy link

Yup, and if we were sticking with the way that useState works it would probably actually return

const [provided, snapshot] = useDraggable({ id, index, })

that way you could easily rename, and it is similar to the way react core works

@gnapse
Copy link

gnapse commented Oct 26, 2018

I wouldn't extrapolate that feature of useState to this case. I'm pretty sure useState does it like that to make it easier to rename the two things to your liking, since is common to use more than one state in several situations.

With something like this hypothetical useDraggable, that's not the case as much. Also, keep in mind there will be more stuff in that returned value, such as isDragging. What if you do not need everything? You'd need to remember the order of the stuff to know how to destructure it. With useState that's not an issue because is just two things and you pretty much always need them both.

@YurkaninRyan
Copy link

YurkaninRyan commented Oct 26, 2018

@gnapse definitely, however the way I picture it, useDraggable would only return two things. For example, check this from the docs.

import { Draggable } from 'react-beautiful-dnd';

<Draggable draggableId="draggable-1" index={0}>
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
    >
      <h4>My draggable</h4>
    </div>
  )}
</Draggable>;

Here we get a provided object as well as a snapshot object. Whether it returns an array or an object doesn't matter so much to me, just that we have access to both.

Unless you are suggesting all the properties get merged on one draggable return object, which is an interesting thought, but different from the current api i'm used to

@gnapse
Copy link

gnapse commented Oct 26, 2018

Oh well, yes. Maybe. Makes more sense now.

@alexreardon
Copy link
Collaborator Author

This seems like a reasonable API

function Card({ itemId, index }) {
  const [provided, snapshot] = useDraggable({ draggableId: itemId, index });
  return (
    <div {...provided.draggableProps} {...provided.dragHandleProps } ref={provided.innerRef}>
      Drag me!
    </div>
  );
}

and

function List({ listId }) {
  const [provided, snapshot] = useDroppable({ draggableId: itemId, index });
  return (
    <div {...provided.droppableProps} ref={provided.innerRef}>
      <p>...</p>
      {provided.placeholder}
    </div>
  );
}

It seems to be the hook equivalent of the render props api we are providing now

@alexreardon alexreardon changed the title RFD: what would a hooks api look like? RFD: What would a React 16.7 hooks api look like? Nov 9, 2018
@trixn86
Copy link

trixn86 commented Nov 14, 2018

@alexreardon

I like this more:

const {provided, snapshot} = useDraggable({ draggableId: itemId, index });

It is not even a single character longer and serves probably >90% of use cases.

I'd argue that the minor advantage of easily renaming the objects returned is not worth the disadvantages returning an array:

  • You can't spread the whole object into a component (or something else) as props.
  • You can't use the dot notation to access its properties when not destructuring it
  • If you were to add another value in the future it will have to be the third one for keeping backwards compatibility. Then it will be hard to get e.g. only the first and third value.
  • Array destructuring is currently slower.

The likelihood of even wanting to rename it is small so I think staying with object spreading and keeping the current names as a convention is better.

If you really feel that you need to rename it you could still do:

const {provided: renamedProvided, snapshot: renamedSnapshot} = useDraggable({ draggableId: itemId, index });

Not too hard if you ask me.

useState is a different story because:

  • you often want to use that hook multiple times in the same component which forces you to give it different names
  • useState has no semantic meaning in your app but you usually want to give it a more meaningful name other than state and setState.

In short that means that in the majority of the cases you actually want to rename the values returned by useState. This is the primary reason they decided not to return an object.

I don't see that being true for higher level hooks like useDraggable which are already very explicit about their use case and what they return. And they are also not used multiple times in the same component very often.

@gnapse
Copy link

gnapse commented Nov 14, 2018

I feel the same way. I fear that because of useState using array destructuring (which is justified in its case for the reasons stated above) now many new hooks-enabled libraries will follow along without strong reasons.

You hardly ever use more than one draggable in the same context, and the same goes for droppable. And even if you did, you'd be better off not destructuring it, give the return value two meaningful names, and use those names' properties in the context they need to be used.

@YurkaninRyan
Copy link

While this does look awesome from the outside, I'm curious how we are going to achieve this effect.

Draggable does a lot more then just feed in those items to it's children, it also wraps those children in the handle as well as the dimension publisher.

I would argue that this is a case where render props/children as a function makes more sense then using hooks, since there is some hierarchy and extra rendering happening.

Any ideas how we could even begin to achieve this effect with hooks?

@YurkaninRyan
Copy link

You hardly ever use more than one draggable in the same context, and the same goes for droppable. And even if you did, you'd be better off not destructuring it, give the return value two meaningful names, and use those names' properties in the context they need to be used.

I actually do agree with the object notation over the array notation in this usecase, gives more extendability down the line.

One thing that is a bit more common then double draggable or double droppable is a draggable inside of a droppable in the same render. But the Droppable and Draggable components expose a provided and a snapshot property.

However, these cases don't feel common enough to warrant array notation

@trixn86
Copy link

trixn86 commented Nov 16, 2018

@YurkaninRyan

Draggable does a lot more then just feed in those items to it's children, it also wraps those children in the handle as well as the dimension publisher.

Any ideas how we could even begin to achieve this effect with hooks?

That is an interesting question. I guess it should be possible to rewrite the DragHandle and DimensionPublisher to be custom hooks themselfs which will then be used by the useDraggable hook internally. Therefore the whole current implementation as class based components has to be rewritten as hooks.

As far as I understood you should be able to transform any class based component logic into a hook including state and lifecycle methods and effectively make the components obsolete. The only problem would be if a component renders a context provider. You can't replace that with a hook.

For DimensionPublisher it would roughly look like that:

const useDraggableDimensionPublisher = ({draggableId, droppableId, type, index, ref}) => {
    // implementation of dimension publisher
    // probably uses `useEffect()` to implement lifecycle side effects
    useEffect(/* lifecycle implementation here */)
}

const useDraggable = ({draggableId, index}) => {
    const {droppableId, type} = useContext(/* ... */);
    const draggableRef = useRef();

    // calls dimension publisher hook to use side effects
    useDraggableDimensionPublisher({draggableId, droppableId, type, index, ref: draggableRef.current})

    // implementation of draggable ...

    return {provided, snapshot};
}

@alexreardon
Copy link
Collaborator Author

My current thinking:

  • Move to hooks for everything internally
  • Expose useDraggable and useDroppable
  • Expose a Draggable and Droppable component with the same function as child api as we currently have. This would allow people to continue to use render props if they want to
// Assuming a useDraggable(props) hook extends

function Draggable({draggableId, type, children}) {
  const [provided, snapshot] = useDraggable({draggableId, type});
  return children(provided, snapshot);
}

@YurkaninRyan
Copy link

I'd love to get working on some of these hooks, what hooks would make the most sense to start with?

@alexreardon alexreardon changed the title RFD: What would a React 16.7 hooks api look like? RFD: Moving a hook api Mar 13, 2019
@alexreardon alexreardon changed the title RFD: Moving a hook api RFD: Moving to a hook api Mar 14, 2019
@alexreardon alexreardon pinned this issue Mar 15, 2019
@alexreardon
Copy link
Collaborator Author

alexreardon commented Mar 15, 2019

Some bad news: there is no useProvider react hook. So there is no way for a hook to setup context.

This is bad because Droppable sets up some context information to be used by Draggable. (type and droppableId).

While we can still move internally to hooks, this might be a dealbreaker to exposing a hook based public api

@YurkaninRyan
Copy link

Could that context information instead be shifted up to DragDropContext and contained in some sort of internal map by droppable id?

@alexreardon
Copy link
Collaborator Author

Interesting @YurkaninRyan

@alexreardon alexreardon changed the title RFD: Moving to a hook api RFD: Moving to a hook api 🎣 Mar 18, 2019
@alexreardon
Copy link
Collaborator Author

Important milestone: everything has been converted to hooks internally, and it mounts without exploding

Screen Shot 2019-03-18 at 2 24 21 pm

@alexreardon
Copy link
Collaborator Author

It then turns into a hot mess when you start to drag

@alexreardon
Copy link
Collaborator Author

I have a branch that is working and passing all tests. I am doing some finial performance testing #1208. You can play with it here: https://deploy-preview-1208--react-beautiful-dnd.netlify.com/?path=/story/single-vertical-list--basic

The branch is currently using react-redux@7-beta and I think we should wait until it is out of beta before leaning on react-redux@7

@alexreardon
Copy link
Collaborator Author

alexreardon commented Apr 2, 2019

@alexreardon
Copy link
Collaborator Author

You can play with the new hooks based implementation:

😍

alexreardon added a commit that referenced this issue Apr 4, 2019
@alexreardon alexreardon mentioned this issue Apr 4, 2019
@alexreardon
Copy link
Collaborator Author

Until we a hook can setup context, we will not be moving to a hook based public API. We might investigate workarounds, but for now the render prop API will remain

@shawninder
Copy link

shawninder commented Apr 13, 2019

Pardon me for intruding, but I don't understand the need for the missing useProvider hook to setup context here.

[...]there is no useProvider react hook. So there is no way for a hook to setup context.

This is bad because Droppable sets up some context information to be used by Draggable. (type and droppableId).

[...]
Until we a hook can setup context, we will not be moving to a hook based public API. [...]

Droppable is already rendering a DroppableDimensionPublisher which is aware of type and droppableId. Why not have it also render a context provider (e.g. <DroppableContext.Provider>)? That way any child (including Draggable) can useContext(DroppableContext).

Am I missing something here?

@alexreardon
Copy link
Collaborator Author

We cannot have a Droppable be a hook right now as it sets up context for a Draggable to consumer. There is no way for a hook to set up context. Droppable has moved over to being a function component that uses hooks, and renders a Context.Provider for a Draggable to consume

@shawninder
Copy link

shawninder commented Apr 15, 2019

Ah, I didn't realize you were trying to make Droppable purely a hook (avoid the Component altogether). I thought the objective was simply to remove the render props syntax.

Thanks for the clarification!

tl;dr
As for setting up context in a hook, perhaps a hook can instead return a component which sets up context? I started thinking along those lines and came up with the following, for what it's worth.

Original syntax

import { useState } from 'react'
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'

export default function App () {
  const [items, setItems] = useState(['one', 'two', 'three'])
  function onDragEnd ({ type, reason, destination, source }) {
    if (type === 'DEFAULT' && reason === 'DROP' && destination) {
      const newItems = Array.from(items)
      const moved = newItems.splice(source.index, 1)
      newItems.splice(destination.index, 0, moved)
      setItems(newItems)
    }
  }
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable droppableId='dropzone'>
        {(droppableProvided, droppableSnapshot) => {
          return (
            <ul ref={droppableProvided.innerRef}>
              {items.map((item, idx) => {
                return (
                  <Draggable draggableId={`draggable-${item}`} index={idx}>
                    {(draggableProvided, draggableSnapshot) => {
                      return (
                        <li
                          key={item}
                          ref={draggableProvided.innerRef}
                          {...draggableProvided.draggableProps}
                          {...draggableProvided.dragHandleProps}
                        >{item}</li>
                      )
                    }}
                  </Draggable>
                )
              })}
              {droppableProvided.placeholder}
            </ul>
          )
        }}
      </Droppable>
    </DragDropContext>
  )
}

Potential new syntax

import { useState } from 'react'
import { useDragDrop } from 'react-beautiful-dnd'

export default function App () {
  const [items, setItems] = useState(['one', 'two', 'three'])
  const [DragDropContext, makeDroppable, makeDraggable] = useDragDrop()

  function onDragEnd ({ type, reason, destination, source }) {
    if (type === 'DEFAULT' && reason === 'DROP' && destination) {
      const newItems = Array.from(items)
      const moved = newItems.splice(source.index, 1)
      newItems.splice(destination.index, 0, moved)
      setItems(newItems)
    }
  }
  const [Droppable, droppableProvided, droppableSnapshot] = makeDroppable({
    id: 'dropzone'
  })
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Droppable>
        <ul ref={droppableProvided.innerRef}>
          {items.map((item, idx) => {
            const [Draggable, draggableProvided, draggableSnapshot] = makeDraggable({
              id: `draggable-${item}`,
              index: idx
            })
            return (
              <Draggable>
                <li
                  key={item}
                  ref={draggableProvided.innerRef}
                  {...draggableProvided.draggableProps}
                  {...draggableProvided.dragHandleProps}
                >{item}</li>
              </Draggable>
            )
          })}
          {droppableProvided.placeholder}
        </ul>
      </Droppable>
    </DragDropContext>
  )
}

Again, I'm not sure how good of an idea this is, I'm just trying to help generate ideas...

@alexreardon
Copy link
Collaborator Author

alexreardon commented Apr 15, 2019

An interesting idea @shawninder. I did consider it, but I was not sure there was much value in this pattern over just keeping with the existing render props api.

The internal hook refactor has shipped in 11.0

For now, until a hook can setup context we won't be able to move to a public hook api. Based on that I will close this issue. I gave it a really good shot!

Deep sea fishing with React Hooks

@mostafah
Copy link

mostafah commented Apr 15, 2019

It seems that useProvider is not going to happen. It is suggested and rejected in facebook/react#14534. And this blog post mentions useProvider as something that doesn’t work well as a hook.

@alexreardon
Copy link
Collaborator Author

It might make sense for hooks not to be able to set up context as an architectural decision. The consequence of the decision is that we cannot move to a purely hooks based api. It is not the end of the world 😊

@alexreardon alexreardon unpinned this issue Apr 15, 2019
@migueloller
Copy link

Why does the API need to be a purely hooks API though? Why not expose just useDraggable and leave Droppable as is?

I do agree it’s not a big deal though, even with hooks being a thing, the render prop API still makes sense when the abstraction is a visual one and not a logical one, which is the case for drag and drop.

Has it been explored to have a potential useDroppable hook that doesn’t set up context but instead registers with the top-level context so that it can then be consumed by useDraggable.

@shawninder
Copy link

Ok, this will be my last attempt with useProvider returning a function which can setup context for a subtree:

useProvider:

function useProvider (Context, value) {
  return (children) => {
    return (
      <Context.Provider value={value}>{children}</Context.Provider>
    )
  }
}

useDroppable:

function useDroppable ({ id }) {
  const droppable = useProvider(DroppableContext, { ... })
  ...
  return [droppable, droppableProvided, droppableSnapshot]
}

useDraggable:

funciton useDraggable ({ id }) {
  const { fromContext } = useContext(DroppableContext)
  ...
  return [draggable, draggableProvided, draggableSnapshot]
}

Usage:

import { useState } from 'react'
import { DragDropContext, useDroppable, useDraggable } from 'react-beautiful-dnd'

export default function App () {
  const [items, setItems] = useState(['one', 'two', 'three'])

  function onDragEnd ({ type, reason, destination, source }) {
    if (type === 'DEFAULT' && reason === 'DROP' && destination) {
      const newItems = Array.from(items)
      const moved = newItems.splice(source.index, 1)
      newItems.splice(destination.index, 0, moved)
      setItems(newItems)
    }
  }
  const [droppable, droppableProvided, droppableSnapshot] = useDroppable({
    id: 'dropzone'
  })
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      {droppable(
        <ul ref={droppableProvided.innerRef}>
          {items.map((item, idx) => {
            const [draggable, draggableProvided, draggableSnapshot] = useDraggable({
              id: `draggable-${item}`,
              index: idx
            })
            return draggable(
              <li
                key={item}
                ref={draggableProvided.innerRef}
                {...draggableProvided.draggableProps}
                {...draggableProvided.dragHandleProps}
              >{item}</li>
            )
          })}
          {droppableProvided.placeholder}
        </ul>
      )}
    </DragDropContext>
  )
}

Working example of useProvider

I don't think this changes the conclusion for this repo, but it may be interesting for people following this discussion.

@pie6k
Copy link

pie6k commented Sep 4, 2020

So what is actual status of this? I can see Hooks PR merged, but I don't really see it in Docs etc.

@trixn86
Copy link

trixn86 commented Sep 4, 2020

@pie6k

So what is actual status of this? I can see Hooks PR merged, but I don't really see it in Docs etc.

As far as I know there is no user facing hooks API yet because react-beautiful-dnd uses react context and there is no way for a hook to set up a context. For that you need to render a ContextProvider which ultimately means you need a component anyways, at least for a Droppable.

The PR only replaced some internal logic that has been managed in class based components with hooks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants