React DnD 是一个react拖拽库, 先来介绍一个来自官网的例子(我进行了一定修改)
现在设置了两个格子,格子内有一个码头棋子,这个棋子可以拖拽到其他格子中
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官网有更详细的介绍)
在Knight组件中使用useDrag hook,通过返回一个ref(drag)使dom元素可被拖拽
在Square组件中使用useDrop hook,使实际dom可被放置。accept参数指定只能放置type为knight的元素
目前为止,基本配置已经完毕
如果你拖拽马头,放到其他格子,没有报错,但马头不会实际跑到其他格子中。实际想让马头移动,需要一个move函数,更新马头的位置
这时候我们看DndDemo组件中的GameContext,如果单独使用React Context对象,需要在顶层组件(DndDemo)中写方法(函数),更新state。react官网的例子中,就是通过setState更新状态
组件内更新state这种方式有几个缺点
- 所有更新方法都要写在组件内,比较臃肿
- 无法复用,比如现在我要写一个move方法,实际更新棋子组件到不同的格子中,如果写在DndDemo组件内,那么这个更新逻辑就无法在其他顶层的container组件中直接使用,必须复制一份
- 如果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的方式不太好,因为会销毁一个