hooks + context + redux + DnD实现拖拽

1,071 阅读3分钟

React DnD 是一个react拖拽库, 先来介绍一个来自官网的例子(我进行了一定修改)

现在设置了两个格子,格子内有一个码头棋子,这个棋子可以拖拽到其他格子中

image.png


import React, { useContext, useReducer } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'

import { spirit } from './models'

import style from './style.css'

const GameContext = React.createContext()

function Knight() {
  const [{ isDragging }, drag] = useDrag({
    item: { type: 'knight' },
        collect: monitor => {
      return {
        isDragging: monitor.isDragging(),
      }
    },
  })
  return <div ref={drag} className={style.knight}>♘</div>
}

function Square({ x, y }) {
  const { state, dispatch } = useContext(GameContext)
  const [{ isOver }, drop] = useDrop({
    accept: 'knight',
    drop: () => dispatch({ type: 'move', x, y }),
    collect: monitor => ({
      isOver: monitor.isOver(),
    }),
  })

  let children = null
  if (state.x === x && state.y === y) {
    children = <Knight x={x} y={y} />
  }
  return <div ref={drop} className={style.square}>{children}</div>
}

function Board({ data }) {
  return (
    <DndProvider backend={HTML5Backend}>
      <Square x={0} y={0} />
      <Square x={1} y={0} />
    </DndProvider>
  )
}

export default function DndDemo(props) {
  const [state, dispatch] = useReducer(spirit.reducer, spirit.state)

  return (
    <GameContext.Provider value={{ state, dispatch }}>
      <Board />
    </GameContext.Provider>
  )
}

首先不要管DndDemo组件内的GameContext 和 useReducer是做什么的

目前一共有三种元素

棋子 Knight 组件

格子 Square 组件

棋盘 Board 组件

看Board组件内的DndProvider组件,这个是dnd提供的拖拽上下文,必须包在需要拖拽和放置目标元素的最外层,backend属性是用于指定拖拽底层是如何实现的,HTML5Backend代表我们直接使用浏览器提供的api(dnd官网有更详细的介绍)

image.png

在Knight组件中使用useDrag hook,通过返回一个ref(drag)使dom元素可被拖拽

在Square组件中使用useDrop hook,使实际dom可被放置。accept参数指定只能放置type为knight的元素

目前为止,基本配置已经完毕

如果你拖拽马头,放到其他格子,没有报错,但马头不会实际跑到其他格子中。实际想让马头移动,需要一个move函数,更新马头的位置


这时候我们看DndDemo组件中的GameContext,如果单独使用React Context对象,需要在顶层组件(DndDemo)中写方法(函数),更新state。react官网的例子中,就是通过setState更新状态


组件内更新state这种方式有几个缺点

  1. 所有更新方法都要写在组件内,比较臃肿
  2. 无法复用,比如现在我要写一个move方法,实际更新棋子组件到不同的格子中,如果写在DndDemo组件内,那么这个更新逻辑就无法在其他顶层的container组件中直接使用,必须复制一份
  3. 如果container是函数式组件,那么可能会限制灵活性,虽然react hook api已经非常接近普通函数,但还是有一定限制


const spirit = {
  state: {
    x: 0,
    y: 0,
  },
  reducer (state, action) {
    const { type, x, y } = action
    switch (type) {
      case 'move':
        return { ...state, x, y }
      default:
        throw new Error();
    }
  }
}

export { spirit }

这是./models.js中的内容,就是一个符合redux规范的js代码,唯一区别就是不再使用react-redux库的observe功能订阅到子组件上


使用context + useReducer天然支持真正的模块化,需要哪一段redux逻辑直接引用过来,使redux可以真正意义上的复用,缺点是一个deducer中想更新其他model中的state变得困难


至此,一个拖拽功能就已实现!

(如果你是为了实现一个需要拖拽的游戏,在Square组件中直接生成children的方式不太好,因为会销毁一个

精灵
并且创建一个新的精灵,但一般需要拖拽的应用是需要在放置目标上copy一个新的原对象)