React Stale State with Custom Hook Event Handling - javascript

It seems I just can't get my head around stale state issues in React as it relates to event handlers and hooks. I conceptually understand what is happening–there is a closure that is capturing the starting value of a state value, and isn't updating when I expect it to.
I am creating a NavBar component onto which I want to add keyboard controls to allow accessibility for sub menus and so forth. I will show the code and then describe what is happening / not happening. I'll also link to a codesandbox for easier forking and debugging.
CodeSandbox
NavBar
const NavBar: React.FC<Props> = ({ children, label }) => {
const {
actions: { createNavItemRef },
state: { activeSubMenuIndex, navBarRef },
} = useNav();
console.log('NAV.BAR', { activeSubMenuIndex });
return (
<nav aria-label={label} ref={navBarRef}>
<NavList aria-label={label} role="menubar">
{children} // NavItems
</NavList>
</nav>
);
};
NavItem
const NavItem: React.FC<Props> = ({ children, hasSubMenu, to }) => {
const ChildrenArray = React.Children.toArray(children);
const {
actions: { handleSelectSubMenu },
state: { activeSubMenuIndex },
} = useNav();
const handleSubMenuToggle = () => {
handleSelectSubMenu(index);
};
return (
<li ref={ref} role="none">
<>
<ParentButton
aria-expanded={activeSubMenuIndex === index}
aria-haspopup="true"
onClick={handleSubMenuToggle}
role="menuitem"
tabIndex={index === 0 ? 0 : -1}
>
{ChildrenArray.shift()}
</ParentButton>
{ChildrenArray.pop()}
</>
</li>
);
};
UseNav
function useNav() {
const navBarRef = useRef<HTMLUListElement>(null);
const [activeSubMenuIndex, setActiveSubMenuIndex] = useState<number | undefined>();
const handleSelectSubMenu = (index?: number) => {
if (!index || activeSubMenuIndex === index) {
setActiveSubMenuIndex(undefined);
} else {
setActiveSubMenuIndex(index);
}
};
useEffect(() => {
const navbar = navBarRef?.current;
navbar?.addEventListener('keydown', () => {
console.log("UseNav", { activeSubMenuIndex });
});
// return () => remove event listener
}, [navBarRef, activeSubMenuIndex]);
return {
actions: {
createNavItemRef,
handleSelectSubMenu,
},
state: {
activeSubMenuIndex,
navBarRef,
},
};
}
This is a somewhat stripped down version of my set up. Ultimately, here's what's going on.
Expectation
I tab onto the first NavItem and it becomes focused. I hit an arrow key (for example) and the log UseNav { activeSubMenuIndex }) logs out correctly as undefined.
Then I click on the NavItem which contains a sub menu. The activeSubMenuIndex updates and in the NavItem the correct sub menu is displayed (based on the activeSubMenuIndex === index conditional).
However, I would expect the NavBar { activeSubMenuIndex }) to log out as well when this NavItem is clicked. But it doesn't.
With the sub menu visible, I hit another arrow key and when the UseNav log is displayed, I would expect it to contain the correct activeSubMenuIndex value, but it is still undefined.
Ultimately, I will need to addEventListeners for keyPress on the NavBar in order to assign keyboard navigation throughout. But if I can't even get the state values updating correctly at this MVP level, then I can't really move forward without making this more cumbersome to work with and debug later.
I know this is an issue of stale state, but I can't find any good articles on this topic that isn't just incrementing a number within the same file. So any help in finally cracking through this wall would be amazing.
Thank you!

Looks like this issue stemmed from the use of the useNav() hook. When I call this hook inside of NavBar references and values are instantiated once. When I call the hook again in NavItem those same refs and values are instantiated again.
In this case, instead of a hook, it would make more sense to wrap this in a context in order to keep the logic out from the UI but keep the components consistent in their data sources.

Related

Reducing renders in child components of a context

I'm trying to delve deeper than the basics into React and am rebuilding a Tree library I had written in plain JS years back. I need to expose an API to users so they can programmatically add/remove nodes, select nodes, etc.
From what I've learned, a ref and context is a good approach. I've built a basic demo following the examples (without ref, for now) but I'm seeing every single tree node re-render when a selection is made, even though no props have changed for all but one.
I've tried a few things like memoizing my tree node component, etc but I feel like I'm failing to understand what's causing the re-render.
I'm using the react dev tools to highlight renders.
Here's a codesandbox demo.
My basic tree node component. I essentially map this for every node I need to show. On click, this calls select() from my context API. The rerender doesn't happen if that select() call is disabled.
const TreeNodeComponent = ({ id, text, children }) => {
console.log(`rendering ${id}`);
const { select } = useTreeContext();
const onClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
select([id]);
},
[select, id]
);
return (
<div className="tree-node" onClick={onClick}>
{text}
{children ? <TreeNodesComponent nodes={children} /> : ""}
</div>
);
};
The important part of my context is the provider:
const TreeContextProvider = ({ children, nodes = [], selectedNodes }) => {
const [allNodes] = useState(nodes);
const [allSelectedNodes, setSelectedNodes] = useState(selectedNodes || []);
const api = useMemo(
() => ({
selected: () => allSelectedNodes,
select: (nodes) => {
setSelectedNodes(Array.from(new Set(allSelectedNodes.concat(nodes))));
}
}),
[allSelectedNodes]
);
const value = useMemo(
() => ({
nodes: allNodes,
selectedNodes: allSelectedNodes,
...api
}),
[allNodes, allSelectedNodes, api]
);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
};
Again, when the call to setSelectedNodes is disabled, the rerenders don't happen. So the entire state update is triggering the render, yet individual props to all but one tree component do not change.
Is there something I can to improve this? Imagine I have 1000 nodes, I can't rerender all of them just to mark one as selected etc.

Dynamically created element is not firing click event in React [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I have many element called infoopts which are just a html i tags, which i attach an event listener to on click so that i can change their style (background color). These are being generated dynamically when i click on a button. I've tried a million things but for some reason none of them will fire the click event.
Here's what i tried:
useEffect(() => {
const infoopts = document.querySelectorAll('.infoopts')
infoopts.forEach(el => {
el.addEventListener('click', function(e) {
console.log('clicked') //never logs this
el.style.background = 'red'
})
})
},[])
console is not even logging the click event. This is supposed to be an amateur issue. Why isn't it creating the click event?
2 Reasons why i am not using React:
I want to style also its neighbor element, onClick will only style that element
If i use state it will change ALL my elements that has infoopts class, i just want to style the clicked one and its neighbor (state is global)
The problem with the code in your question is that .infoopts might not even exist when this component is mounted. In that case, querySelectorAll would find nothing and your event handler would never fire because it was never attached to anything.
Without knowing the source of your data, or what your component hierarchy looks like, below is one possible example of a naive implementation.
Hold onto some local component state to determine which item in the list is selected, and then render the UI from that state.
The rendered DOM nodes are simply a reflection of the component's state. To update any of the DOM nodes, simply update that state.
import React, { useState } from "react";
export default function App() {
const [items, setItems] = useState([
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false },
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false },
{ title: "foo", selected: false },
{ title: "bar", selected: false },
{ title: "buzz", selected: false }
]);
const handleClick = (i) => {
const updated = [...items];
updated[i].selected = !items[i].selected;
setItems(updated);
};
const determineBgColor = (i) => {
const itemIsSelected = items[i].selected;
const neighbourIsSelected =
items[i - 1]?.selected || items[i + 1]?.selected;
if (itemIsSelected) {
return "darkred";
} else if (neighbourIsSelected) {
return "bisque";
}
return "darksalmon";
};
return (
<div className="App">
<ul>
{items.map((item, i) => (
<li key={i}>
<button
style={{ background: determineBgColor(i) }}
onClick={() => handleClick(i)}
>
{item.title}
</button>
</li>
))}
</ul>
</div>
);
}
Alternatively, the state might already be kept in a parent component. In that case it might be passed into this component as a prop, and this component is simply rendering based on the value of the data in that prop.
Then you would need to find a way to update the state in the parent component, and the changes will flow from the parent, to the child, and the rendered DOM will update accordingly.
Either way, the same principles apply. When using React you should aim to be writing declarative components that react to their state & props, as opposed to attempting to imperatively affect the DOM yourself.
I highly recommend that you read https://reactjs.org/docs/thinking-in-react.html if you haven't already.

Context Api state is not changing

im trying to call a function called deleteTask inside the Context Provider, from a component that consumes the context using the useContext hook, which deletes a certain item from an array in the state of the context provider, but when i do it, the state of the provider doesnt change at all, i try to follow the problem and the function excecutes but it seems like if it was excecuting in the scope of a copied Provider? Also tried a function to add a task and im having the same issue. I also added a function to set the active task, and i dont know why that one did work, while the others dont. I dont really know whats happening, here is the code, pleeeeease help me:
tasks-context.jsx
import React, { useState } from 'react';
import { useEffect } from 'react';
const dummyTasks = [{
task: {
text: 'hello',
},
key: 0,
isActive: false
},
{
task: {
text: 'hello 2',
},
key: 1,
isActive: false
}];
export const TasksContext = React.createContext({ });
export const TasksProvider = ( props ) => {
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
//NOT WORKING
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
//THIS ONE WORKS (??)
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
ficha.isActive = false;
}
return task;
});
setTask( newTaskArray );
};
return ( <TasksContext.Provider
value={{ tasks,
activeTask,
addTask,
deleteTask,
handleSelectTask}}>
{props.children}
</TasksContext.Provider>
);
};
the "main"
Main.jsx
import React from 'react';
import './assets/styles/gestion-style.css';
import './assets/styles/icons.css';
import { TasksProvider } from '../../Context/tasks-context';
import TaskContainer from './components/taskContainer.jsx';
function Main( props ) {
return (
<TasksProvider>
<TaskContainer />
</TasksProvider>
);
}
the task container maps the array of tasks:
TaskContainer.jsx
import React, { useContext, useEffect } from 'react';
import TaskTab from './TaskTab';
import { TasksContext } from '../../Context/tasks-context';
function TaskContainer( props ) {
const { tasks } = useContext( TasksContext );
return (
<div className="boxes" style={{ maxWidth: '100%', overflow: 'hidden' }}>
{tasks? tasks.map( taskTab=>
( <TaskTab task={taskTab.task} isActive={taskTab.isActive} key={taskTab.key} taskTabKey={taskTab.key} /> ))
:
null
}
</div>
);
}
export default TaskContainer;
And the task component from which i call the context function to delete:
TaskTab.jsx
import React, { useContext } from 'react';
import { TasksContext } from '../../Context/tasks-context';
function TaskTab( props ) {
let { task, isActive, taskTabKey } = props;
const { handleSelectTask, deleteTask } = useContext( TasksContext );
const selectTask = ()=>{
handleSelectTask( task, taskTabKey );
};
const handleDelete = () =>{
deleteTask( taskTabKey );
};
return (
<div onClick={ selectTask }>
<article className={`${task.type} ${isActive ? 'active' : null}`}>
<p className="user">{task.text}</p>
<button onClick={handleDelete}>
<i className="icon-close"></i>
</button>
</article>
</div>
);
}
export default TaskTab;
Thanks for the great question!
What is happening here is understandably confusing, and it took me a while to realize it myself.
TL;DR: handleSelectTask in the Provider is being called every time a button is clicked for deleteTask because of event propagation. handleSelectTask isn't using the state that has been modified by deleteTask, even though it's running after it, because it has closure to the initial tasks array.
Quick Solution 1
Stop the event from propagating from the delete button click to the TaskTab div click, which is probably the desired behavior.
// in TaskTab.jsx
const handleDelete = (event) => {
event.stopPropagation(); // stops event from "bubbling" up the tree
deleteTask(taskTabKey);
}
In the DOM (and emulated by React as well), events "bubble" up the tree, so that parent nodes can handle events coming from their child nodes. In the example, the <button onClick={handleDelete}> is a child of the <div onClick={selectTask}>, which means that when the click event is fired from the button, it will first call the handleDelete function like we want, but it will also call the selectTask function from the parent div afterwards, which is probably unintended. You can read more about event propagation on MDN.
Quick Solution 2
Write the state updates to use the intermediary state value at the time they are called.
// in tasks-context.jsx
const deleteTask = ( taskToDeleteKey ) => {
setActiveTask(null);
// use the function version of setting state to read the current value whenever it is run
setTasks((stateTasks) => stateTasks.filter(task => task.key !== taskToDeleteKey));
}
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// updated to use the callback version of the state update
setTasks((stateTasks) => stateTasks.map( task => {
// set the correct one to active
}));
};
Using the callback version of the setTasks state update, it will actually read the value at the time the update is being applied (including and especially in the middle of an update!), which, since the handleSelectTask is called after, means that it actually sees the array that has already been modified by the deleteTask that ran first! You can read more about this callback variant of setting state in the React docs (hooks) (setState). Note that this "fix" will mean that your component will still call handleSelectTask even though the task has been deleted. It won't have any ill-effects, just be aware.
Let's walk through what's happening in a bit more detail:
First, the tasks variable is created from useState. This same variable is used throughout the component, which is totally fine and normal.
// created here
const [ tasks, setTasks ] = useState( dummyTasks );
const [ activeTask, setActiveTask ] = useState();
const deleteTask = ( taskToDeleteKey ) =>{
setActiveTask( null );
// referenced here, no big deal
setTasks( tasks.filter( task => task.key !== taskToDeleteKey ));
};
const handleSelectTask = ( taskToSelect, key ) =>{
setActiveTask( taskToSelect );
// tasks is referenced here, too, awesome
const newTaskArray = tasks.map( task => {
if( task.key === key ){
task.isActive = true;
}else{
task.isActive = false;
}
return task;
});
setTasks( newTaskArray );
};
Where the trouble comes in, is that if both of the functions are trying to update the same state value in the same render cycle, they will both be referencing the original value of the tasks array, even if the other function has attempted to update the state value! In your case, because the handleSelectTask is running after deleteTask, this means that handleSelectTask will update state using the array that hasn't been modified! When it runs, it will still see two items in the array, since the tasks variable won't change until the update is actually committed and everything rerenders. This makes it look like the delete portion isn't functioning, when really its effect is just being discarded since handleSelectTask isn't aware that the delete happened before it.
Lucas, this is not an issue with Context or Provider.
The problem that you are facing is actually a mechanism known as event bubbling where the current handler executes followed by parent handlers.
More info on event bubbling could be found here. https://javascript.info/bubbling-and-capturing.
In your case first, the handleDelete function gets called followed by handleSelect function.
Solution: event.stopPropagation();
Change your handleDelete and handleSelect function to this
const selectTask = () => {
console.log("handle select called");
handleSelectTask(task, taskTabKey);
};
const handleDelete = event => {
console.log("handle delete called");
event.stopPropagation();
deleteTask(taskTabKey);
};
Now check your console and you will find only the handle delete called will print and this would solve your problem hopefully.
If it still doesn't work then do let me know. I will create a codesandbox version for you.
Happy Coding.

React Navigation 2: How to check previous scene and to disable tab change

I have a tab navigation. One of my tabs has a form and I would like to disable navigate event if my form data is not saved.
In ver.1, the tabBarOnPress method provides previousScene, scene and jumpToIndex, so I was able to check which scene I am leaving and to access its props.
Now in ver.2, the tabBarOnPress method provides the navigation props for the scene, but the previous scene prop is missing :/
navigationOptions: {
tabBarOnPress: ({ navigation, defaultHandler }) => {
// Check the previous screen
// If I am leaving the home screen and the user has unsaved data
// disable tab navigation
// else change to the pressed tab
},
},
Also, I tried with the navigation event listeners but the NAVIGATE action is already dispatched:
props.navigation.addListener('willBlur', () => {
// Disable tab switching;
}),
Simple snack: https://snack.expo.io/#hristoeftimov/handle-tab-changes-in-react-navigation-v2
Any solutions how to disable tab switching before leave a tab?
I have found a much simpler way, using the getStateForAction.
const defaultGetStateForAction = MainStack.router.getStateForAction;
MainStack.router.getStateForAction = (action, state) => {
if (!state) {
return defaultGetStateForAction(action, state);
}
if (
action.type === NavigationActions.NAVIGATE
&& state.routes[state.index].key === 'HomeTab'
) {
const tab = state.routes[state.index];
const currentRoute = tab.routes[tab.index];
const currentRouteParams = currentRoute.params;
if (currentRouteParams && currentRouteParams.isNavigationDisabled) {
return currentRouteParams.showConfirmationDialog(action);
}
}
return defaultGetStateForAction(action, state);
}
Every time when I switch between the tabs it jumps into getStateForAction where I can access the leaving tab (from state) and the next screen (from action).
So, when my action is NAVIGATE and the leaving screen/route is HoneTab I can change/disable the default state for action and to trigger showConfirmationDialog() - This is a function that I can set as a route parameter to my HomeTab screen.
The navigation object contains the data you need, as it holds the navigation state prior to the navigation to the new tab. This navigation state has both the screen from which you're navigating and its params.
In order to get the state you can use the following function:
function getCurrentRoute(navState) {
if (!navState) {
return null;
}
const route = navState.routes[navState.index];
if (route.routes) {
return getCurrentRoute(route); // nested routes
} else {
return {
name: route.routeName,
params: { ...route.params }
};
}
}
So now you can use this function inside the onPress handler. Something like this:
navigationOptions: {
tabBarOnPress: ({ navigation, defaultHandler }) => {
const currentRoute = getCurrentRoute(navigation.state);
if (currentRoute.name !== 'Home' || !currentRoute.params.isNavigationDisabled) {
defaultHandler();
}
}
}
Of course this means that you'll need to manage a navigation param called isNavigationDisabled in your Home screen by using this.props.navigation.setParams method.
Also, I hope I was correct with the screen name, if not just debug it.

Get element position in the DOM on React DnD drop?

I'm using React DnD and Redux (using Kea) to build a formbuilder. I've the drag & drop portion working just fine, and I've managed to dispatch an action when an element drops, and I render the builder afterwards using state that the dispatch changed. However, in order to render elements in the correct order, I (think I) need to save the dropped elements position relative to it's siblings but I'm unable to figure out anything that isn't absolutely insane. I've experimented with refs and querying the DOM with the unique ID (I know I shouldn't), but both approaches feel pretty terrible and do not even work.
Here's a simplified representation of my app structure:
#DragDropContext(HTML5Backend)
#connect({ /* redux things */ })
<Builder>
<Workbench tree={this.props.tree} />
<Sidebar fields={this.props.field}/>
</Builder>
Workbench:
const boxTarget = {
drop(props, monitor, component) {
const item = monitor.getItem()
console.log(component, item.unique, component[item.unique]); // last one is undefined
window.component = component; // doing it manually works, so the element just isn't in the DOM yet
return {
key: 'workbench',
}
},
}
#DropTarget(ItemTypes.FIELD, boxTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}))
export default class Workbench extends Component {
render() {
const { tree } = this.props;
const { canDrop, isOver, connectDropTarget } = this.props
return connectDropTarget(
<div className={this.props.className}>
{tree.map((field, index) => {
const { key, attributes, parent, unique } = field;
if (parent === 'workbench') { // To render only root level nodes. I know how to render the children recursively, but to keep things simple...
return (
<Field
unique={unique}
key={key}
_key={key}
parent={this} // I'm passing the parent because the refs are useless in the Field instance (?) I don't know if this is a bad idea or not
/>
);
}
return null;
}).filter(Boolean)}
</div>,
)
// ...
Field:
const boxSource = {
beginDrag(props) {
return {
key: props._key,
unique: props.unique || shortid.generate(),
attributes: props.attributes,
}
},
endDrag(props, monitor) {
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
console.log(dropResult);
if (dropResult) {
props.actions.onDrop({
item,
dropResult,
});
}
},
}
#connect({ /* redux stuff */ })
#DragSource(ItemTypes.FIELD, boxSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Field extends Component {
render() {
const { TagName, title, attributes, parent } = this.props
const { isDragging, connectDragSource } = this.props
const opacity = isDragging ? 0.4 : 1
return connectDragSource(
<div
className={classes.frame}
style={{opacity}}
data-unique={this.props.unique || false}
ref={(x) => parent[this.props.unique || this.props.key] = x} // If I save the ref to this instance, how do I access it in the drop function that works in context to boxTarget & Workbench?
>
<header className={classes.header}>
<span className={classes.headerName}>{title}</span>
</header>
<div className={classes.wrapper}>
<TagName {...attributes} />
</div>
</div>
)
}
}
Sidebar isn't very relevant.
My state is a flat array, consisting of objects that I can use to render the fields, so I'm reordering it based on the element positions in the DOM.
[
{
key: 'field_type1',
parent: 'workbench',
children: ['DAWPNC'], // If there's more children, "mutate" this according to the DOM
unique: 'AWJOPD',
attributes: {},
},
{
key: 'field_type2',
parent: 'AWJOPD',
children: false,
unique: 'DAWPNC',
attributes: {},
},
]
The relevant portion of this question revolves around
const boxTarget = {
drop(props, monitor, component) {
const item = monitor.getItem()
console.log(component, item.unique, component[item.unique]); // last one is undefined
window.component = component; // doing it manually works, so the element just isn't in the DOM yet
return {
key: 'workbench',
}
},
}
I figured I'd just get the reference to the element somehow, but it doesn't seem to exist in the DOM, yet. It's the same thing if I try to hack with ReactDOM:
// still inside the drop function, "works" with the timeout, doesn't without, but this is a bad idea
setTimeout(() => {
const domNode = ReactDOM.findDOMNode(component);
const itemEl = domNode.querySelector(`[data-unique="${item.unique}"]`);
const parentEl = itemEl.parentNode;
const index = Array.from(parentEl.children).findIndex(x => x.getAttribute('data-unique') === item.unique);
console.log(domNode, itemEl, index);
});
How do I achieve what I want?
Apologies for my inconsistent usage of semicolons, I don't know what I want from them. I hate them.
I think the key here is realizing that the Field component can be both a DragSource and a DropTarget. We can then define a standard set of drop types that would influence how the state is mutated.
const DropType = {
After: 'DROP_AFTER',
Before: 'DROP_BEFORE',
Inside: 'DROP_INSIDE'
};
After and Before would allow re-ordering of fields, while Inside would allow nesting of fields (or dropping into the workbench).
Now, the action creator for handling any drop would be:
const drop = (source, target, dropType) => ({
type: actions.DROP,
source,
target,
dropType
});
It just takes the source and target objects, and the type of drop occurring, which will then be translated into the state mutation.
A drop type is really just a function of the target bounds, the drop position, and (optionally) the drag source, all within the context of a particular DropTarget type:
(bounds, position, source) => dropType
This function should be defined for each type of DropTarget supported. This would allow each DropTarget to support a different set of drop types. For instance, the Workbench only knows how to drop something inside of itself, not before or after, so the implementation for the workbench could look like:
(bounds, position) => DropType.Inside
For a Field, you could use the logic from the Simple Card Sort example, where the upper half of the DropTarget translates to a Before drop while the lower half translates to an After drop:
(bounds, position) => {
const middleY = (bounds.bottom - bounds.top) / 2;
const relativeY = position.y - bounds.top;
return relativeY < middleY ? DropType.Before : DropType.After;
};
This approach also means that each DropTarget could handle the drop() spec method in the same manner:
get bounds of the drop target's DOM element
get the drop position
calculate the drop type from the bounds, position, and source
if any drop type occurred, handle the drop action
With React DnD, we have to be careful to appropriately handle nested drop targets since we have Fields in a Workbench:
const configureDrop = getDropType => (props, monitor, component) => {
// a nested element handled the drop already
if (monitor.didDrop())
return;
// requires that the component attach the ref to a node property
const { node } = component;
if (!node) return;
const bounds = node.getBoundingClientRect();
const position = monitor.getClientOffset();
const source = monitor.getItem();
const dropType = getDropType(bounds, position, source);
if (!dropType)
return;
const { onDrop, ...target } = props;
onDrop(source, target, dropType);
// won't be used, but need to declare that the drop was handled
return { dropped: true };
};
The Component class would end up looking something like this:
#connect(...)
#DragSource(ItemTypes.FIELD, {
beginDrag: ({ unique, parent, attributes }) => ({ unique, parent, attributes })
}, dragCollect)
// IMPORTANT: DropTarget has to be applied first so we aren't receiving
// the wrapped DragSource component in the drop() component argument
#DropTarget(ItemTypes.FIELD, {
drop: configureDrop(getFieldDropType)
canDrop: ({ parent }) => parent // don't drop if it isn't on the Workbench
}, dropCollect)
class Field extends React.Component {
render() {
return (
// ref prop used to provide access to the underlying DOM node in drop()
<div ref={ref => this.node = ref}>
// field stuff
</div>
);
}
Couple things to note:
Be mindful of the decorator order. DropTarget should wrap the component, then DragSource should wrap the wrapped component. This way, we have access to the correct component instance inside drop().
The drop target's root node needs to be a native element node, not a custom component node.
Any component that will be decorated with the DropTarget utilizing configureDrop() will require that the component set its root node's DOM ref to a node property.
Since we are handling the drop in the DropTarget, the DragSource just needs to implement the beginDrag() method, which would just return whatever state you want mixed into your application state.
The last thing to do is handle each drop type in your reducer. Important to remember is that every time you move something around, you need to remove the source from its current parent (if applicable), then insert it into the new parent. Each action could mutate the state of up to three elements, the source's existing parent (to clean up its children), the source (to assign its parent reference), and the target's parent or the target if an Inside drop (to add
to its children).
You also might want to consider making your state an object instead of an array, which might be easier to work with when implementing the reducer.
{
AWJOPD: { ... },
DAWPNC: { ... },
workbench: {
key: 'workbench',
parent: null,
children: [ 'DAWPNC' ]
}
}

Categories