React-dnd drop failure state - reactjs

I'm using the react-dnd library to manage drag/drop, and running into what feels like a limitation, but wanting to make sure I'm not just overlooking something.
I've got a React component implementing useDrop with a fairly complicated set of rules inside canDrop. When the user releases the mouse and canDrop returns false, I'd love to be able to provide a detailed error message so they know why they weren't allowed to drop the item, but drop isn't called (expected behavior, per the docs), and there doesn't appear to be any way to provide context back to the useDrag handler's end function either.
I could obviously fire the error message from canDrop itself, but that function is called hundreds of times for each drag, so that introduces issues like debouncing etc. that I'd prefer to avoid.
So my question is, have I just run into a limitation of the library, or am I missing an event/hook somewhere?

I had the same problem and here is my solution:
canDrop contains the complex logic to determine if the item...can drop.
Add the item id (itemId) to the collect function passed to useDrop and use that id to determine what error message to show based on the combination of !canDrop && itemId==='some specific id'.
My useDrop hook:
const [{ isOver, canDrop, itemId }, drop] = useDrop(
() => ({
accept: [{valid ids go here}],
drop: (item, monitor) => {
// omitted
},
canDrop: (item, monitor) => {
// canDrop logic
},
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
itemId: monitor.getItem()?.id,
}),
}),
[]
);
Then in your component render:
return (
<>
{canDrop && <span>You're good to drop</span>}
{!canDrop && itemId === 'some condition' && <span>Error message specific to this condition</span>}
{!canDrop && itemId === 'some other condition' && <span>Error message specific to this condition</span>}
</>
)
For my scenario the item's id lets me determine the error message I should show but I think you could also return the entire item from the collect function.

Related

Filtered list: how best to keep keyboard nav in sync with the list as rendered?

I'm building out keyboard navigation for a composable React combobox component. As it stands, it works except when the visible list of options is filtered down to match text field input. I'm using React.Children in the callback below to build an array corresponding to my options list; this works well on initialization, but after filtering (and re-rendering the options that match the filter) the result when I call it still includes all items, rendered or not, with the props they initially had. (I had the "bright idea" of giving unrendered children a tabindex of -2 and filtering that way, but this only works if the values are updated...)
Is there a better way than React.Children to populate my array of IDs and values/labels for my options, and to update it based on what was most recently rendered?
If so, where could I learn about it?
If not:
how can I access the current state, rather than a stale one?
what's the best way of pointing to the specific children in question? The method I'm using looks brittle.
const updateOptions = useCallback(
() =>
// set the optionStateMap from children
React.Children.forEach(children, child => {
if (!React.isValidElement(child)) return;
if (!child.props.children) return;
const optionsArray: Array<optionStateProps> = [];
child.props.children[0].forEach( // will break if my list is not the first child with children; d'oh
(option: { props: { id: string; label: string, tabIndex: number} }) => {
if (option.props.tabIndex < -1) return;
optionsArray.push({
id: option.props.id,
label: option.props.label
});
}
);
setOptionStateMap(optionsArray);
}),
[children]
);
Thanks for looking!

react native Why does my async code hang the rendering?

General :
TL;DR: async code hangs rendering.
I have this component with a Modal and inside the Modal it renders a list of filters the user can choose from. When pressing a filter the color of the item changes and it adds a simple code(Number) to an array. The problem is that the rendering of the color change hangs until the logic that adds the code to the array finishes.
I don't understand why adding a number to an array takes between a sec and two.
I don't understand why the rendering hangs until the entire logic behind is done.
Notes: I come from a Vue background and this is the first project where I'm using react/react-native. So if I'm doing something wrong it would be much appreciated if someone points that out
Snack that replicates the issue :
Snack Link
My code for reference :
I use react-native with expo managed and I use some native-base components for the UI.
I can't share the whole code source but here are the pieces of logic that contribute to the problem :
Parent : FilterModal.js
The rendering part :
...
<Modal
// style={styles.container}
visible={modalVisible}
animationType="slide"
transparent={false}
onRequestClose={() => {
this.setModalVisible(!modalVisible);
}}
>
<Center>
<Pressable
onPress={() => this.setModalVisible(!modalVisible)}
>
<Icon size="8" as={MaterialCommunityIcons} name="window-close" color="danger.500" />
</Pressable>
</Center>
// I use sectionList because the list of filters is big and takes time to render on the screen
<SectionList
style={styles.container}
sections={[
{ title: "job types", data: job_types },
{ title: "job experience", data: job_experience },
{ title: "education", data: job_formation },
{ title: "sector", data: job_secteur }
]}
keyExtractor={(item) => item.id}
renderItem={({ item, section }) => <BaseBadge
key={item.id}
pressed={this.isPressed(section.title, item.id)}
item={item.name}
code={item.id}
type={section.title}
add={this.addToFilters.bind(this)}
></BaseBadge>}
renderSectionHeader={({ section: { title } }) => (
<Heading color="darkBlue.400">{title}</Heading>
)}
/>
</Modal>
...
The logic part :
...
async addToFilters(type, code) {
switch (type) {
case "job types":
this.addToTypesSelection(code);
break;
case "job experience":
this.addToExperienceSelection(code);
break;
case "formation":
this.addToFormationSelection(code);
break;
case "sector":
this.addToSectorSelection(code);
break;
default:
//TODO
break;
}
}
...
// the add to selection methods look something like this :
async addToTypesSelection(code) {
if (this.state.jobTypesSelection.includes(code)) {
this.setState({ jobTypesSelection: this.state.jobTypesSelection.filter((item) => item != code) })
}
else {
this.setState({ jobTypesSelection: [...this.state.jobTypesSelection, code] })
}
}
...
Child :
The rendering Part
render() {
const { pressed } = this.state;
return (
< Pressable
// This is the source of the problem and read further to know why I used the setTimeout
onPress={async () => {
this.setState({ pressed: !this.state.pressed });
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
<Badge
bg={pressed ? "primary.300" : "coolGray.200"}
rounded="md"
>
<Text fontSize="md">
{this.props.item}
</Text>
</Badge>
</Pressable >
);
};
Expected outcome :
The setState({pressed:!this.state.pressed}) finishes the rendering of the item happens instantly, the rest of the code happens after and doesn't hang the rendering.
The change in the parent state using the add code to array can happen in the background but I need the filter item ui to change instantly.
Things I tried :
Async methods
I tried making the methods async and not await them so they can happen asynchronously. that didn't change anything and seems like react native ignores that the methods are async. It hangs until everything is done all the way to the method changing the parent state.
Implementing "event emit-listen logic"
This is the first app where I chose to use react/react-native, coming from Vue I got the idea of emitting an event from the child and listening to it on the parent and execute the logic that adds the code to the array.
This didn't change anything, I used eventemitter3 and react-native-event-listeners
Using Timeout
This is the last desperate thing I tried which made the app useable for now until I figure out what am I doing wrong.
basically I add a Timeout after I change the state of the filter component like so :
...
< Pressable
onPress={async () => {
// change the state this changes the color of the item ↓
this.setState({ pressed: !this.state.pressed });
// this is the desperate code to make the logic not hang the rendering ↓
setTimeout(() => {
this.props.add(this.props.type, this.props.code);
});
}}
>
...
Thanks for reading, helpful answers and links to the docs and other articles that can help me understand better are much appreciated.
Again I'm new to react/react-native so please if there is some concept I'm not understanding right point me in the right direction.
For anyone reading this I finally figured out what was the problem and was able to solve the issue for me.
The reason the rendering was getting hang is because the code that pushes to my array took time regardless of me making it async or not it was being executed on the main thread AND that change was triggering screen re-render which needed to wait for the js logic to finish.
The things that contribute to the solution and improvement are :
Make the array (now a map{}) that holds the selected filters stateless, in other words don't use useState to declare the array, instead use good old js which will not trigger any screen re-render. When the user applies the filters then push that plain js object to a state or context like I'm doing and consume it, doing it this way makes sure that the user can spam selecting and deselecting the filters without hanging the interactions.
first thing which is just a better way of doing what I needed is to make the array a map, this doesn't solve the rerender issue.

React with typescript, button function doesn't work

I'm slowly starting to learn TS and implement it to current project however I stuck and don't really understand what is wrong. Basically I have button which has dataset "mode". After clicking on button I launch confirm bar (confirm bar is not TSX yet)
<Button
height={50}
data-mode="MEMORY"
onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
ConfirmBar('You sure?', supportCommands, e)
}
>
Format
</Button>
const ConfirmBar = (message, action, parameter) =>
confirmAlert({
customUI: ({ onClose }) => {
return (
<ConfirmContainer>
<Header main>{message}</Header>
<ConfirmationButton
confirm
onClick={() => {
action(parameter);
onClose();
}}
>
Yes
</ConfirmationButton>
<ConfirmationButton onClick={onClose}>No</ConfirmationButton>
</ConfirmContainer>
);
},
});
In case of yes I wish to launch function to proceed request, it worked correctly before typescript but now it throws error. I wish to get access to dataset attribute and would be glad if you guys help me and explain me why it doesn't want to work now after added typescript
const supportCommands = (el: React.MouseEvent<HTMLButtonElement>) => {
// Tried already to use el.persist(), with target and currentTarget; here is example with attempting to assign value to variable but also doesn't work.
const target = el.currentTarget as HTMLButtonElement;
let elVal = target.getAttribute('data-mode');
console.log(elVal, 'ELL');
};
And that's the error I occur:
Warning: This synthetic event is reused for performance reasons. If
you're seeing this, you're accessing the method currentTarget on a
released/nullified synthetic event. This is a no-op function. If you
must keep the original synthetic event around, use event.persist().
See fb.me/react-event-pooling for more information.
I understand that React has own system of SynthesisEvents but I thought they cause problems during asynchronous requests like with timers etc, in this situation I see no reason why it makes problem
EDIT: I made it work by adding to button e.currentTarget, and then in function just did el.dataset, now just trying to figure out what kind of type is that
This waring is because you are reusing Event object.
You passed it here ConfirmBar('You sure?', supportCommands, e)
And you reused it here action(parameter);
I don't know what do you need from paramenter but I guess it could be like this:
onClick={(e) => {
action(e);
onClose();
}}
I have never needed to use event of onClick. The only idea I can imagine is for preventDefault or stopPropagation

Mutable global state causing issues with array length

I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?

React Parent-Child Relationship and Encapsulation

I have a React design problem that I am trying to solve, and I am running into an issue counter-intuitive to the idea of encapsulation as I try to breakdown an existing component into parent-child to support an additional use case. I am using React 15.3.2 with Redux, ES6, and ImmutableJS. First, I will illustrate the design problem, then I will provide snippets to illustrate why I feel that I have the need to get data back from children and how that is good for encapsulation, and the roadblock I am hitting.
I have read this stackoverflow which has detailed explanation on why passing data from children to parent component is not a good idea,
Pass props to parent component in React.js
But I have some concerns, which I will discuss at the end.
Design:
I am starting with a CheckboxSelect component. The Title bar's text depends on the checked items.
Closed:
Open with selections (current implementation):
To support additional use-case, the dropdown will now open up with more stuff.
Open with selections (new update):
Initial Code:
I am starting with a CheckboxSelect controlled component with the following props interface:
CheckboxSelect.jsx:
CheckboxSelect.propTypes = {
// what title to display by default with no selection
defaultTitle: PropTypes.string.isRequired, ie. "Selected Scopes"
// array of selected options, ie. [{key: "comedy", name: "comedy", checked: false }, ...]
options: PropTypes.array.isRequired,
// handler for when the user checks a selection, this will update
// the reducer state, which causes the options prop to be refreshed and
// passed in from the outer container
onCheck: PropTypes.func.isRequired,
onUncheck: PropTypes.func.isRequired,
onCheckAll: PropTypes.func,
onUncheckAll: PropTypes.func,
className: PropTypes.string,
// controls the open state of the dropdown
open: PropTypes.bool,
// handler for when the user clicks on the dropdown button, this will update the reducer state,
// which causes the open prop to be refreshed and passed in from the outer container
onClick: PropTypes.func,
onCancel: PropTypes.func,
};
// there is currently some logic inside the component to generate the title to display
// produces "comedy, action"
getTitle() {
const { defaultTitle } = this.props;
const checked = this.getChecked();
let fullTitle;
if (this.allChecked() || this.allUnchecked()) {
fullTitle = `${defaultTitle } (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
getChecked() {
const { options } = this.props;
return options.filter(option => option.checked);
}
allChecked() {
const { options } = this.props;
return this.getChecked().length === options.length;
}
allUnchecked() {
return this.getChecked().length === 0;
}
ApplicationContainer.jsx (where the component is being used):
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const movieScopes = this.applicableScopes.push(scope.key);
this.onUpdateField('applicable_scopes', movieScopes);
}
render() {
...
<CheckboxSelect
defaultTitle="Selected Scopes"
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
open={store.get('scopeDropdownOpen')}
</CheckboxSelect>
}
New Code:
To support the new layout, I would like to break the existing component into two: a DynamicDropdown that contains CheckboxSelect2 as one of the children, along with any other elements that may be dropped down. Here is how the new code will look inside the ApplicationContainer.
ApplicationContainer.jsx
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const {store } = this.props;
const cachedApplicableScopes = store.get('cachedApplicableScopes').push(scope.key);
store.get('cachedApplicableScopes').push(scope.key);
this.actions.setCachedApplicableScopes(cachedApplicableScopes);
// wait until apply is clicked before update
}
render() {
...
<DynamicDropdown
className="scope-select"
title={this.scopeDropdownTitle()}
open={store.get('scopeDropdownOpen')}
onClick={this.onScopeDropdownClick}
onCancel={this.onScopeDropdownCancel}
>
<CheckboxSelect2
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
visble={store.get('scopeDropdownOpen')}
/>
// ... other children like confirmation message and buttons
</DynamicDropdown>
}
// logic related to CheckboxSelect2 title generation moved to the ApplicationContainer. Not ideal in my opinion as it breaks encapsulation. Further discussions in the Choices section
getScopesChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return options.filter(option => option.checked);
}
scopesAllChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return this.getScopesChecked().length === options.length;
}
scopesAllUnchecked() {
return this.getScopesChecked().length === 0;
}
scopeDropdownTitle() {
const defaultTitle = "Selected Scopes";
const checked = this.getScopesChecked();
let fullTitle;
if (this.scopesAllChecked() || this.scopesAllUnchecked()) {
fullTitle = `${defaultTitle} (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
Problem:
The problem I have is with populating the title props of the DynamicDropdown element with the New Code, since it depends on the result of the CheckboxSelect2 selection.
Keep in mind CheckboxSelect2 is a dumb controlled component, we pass an onCheck handler to it. The this.onSelectScope inside the ApplicationContainer, is responsible for updating the reducer state of what has been checked, which in turn refresh the props and causes the DynamicDropdown and CheckboxSelect2 to be re-rendered.
Choices:
In the old code, there is a group of logic used to figure out the title to display for the dropdown. Here are the choices I am presented with:
To keep encapsulation of letting the CheckboxSelect2 summarize the
title, I tried initially keeping the same title logic inside
CheckboxSelect2, and accessing it via a ref.
ApplicationContainer.jsx
<DynamicDropdown
title={this.childCheckboxSelect.getTitle()}
>
<CheckboxSelect2
ref={(childCheckboxSelect) => this.childCheckboxSelect = childCheckboxSelect}
>
</DynamicDropdown>
At the time that DynamicDropdown is re-rendered, CheckboxSelect2
hasn't received the new props yet from the parent via the one-way
data-flow, so as a child, it actually cannot generate the most
up-to-date title for the DynamicDropdown based on what has been
checked. With this implementation, my title is one-step behind what
was actually checked.
As shown in the ApplicationContainer.jsx for the New Code section
above, the logic for the scopeDropdownTitle could be moved out from
CheckboxSelect2 to ApplicationContainer.jsx, so it sits a level
above DynamicDropdown, enabling the DynamicDropdown to get the
updated title from the reducer state as it renders. While this
solution works, it totally breaks my view on encapsulation, where
the responsibility for determining what title to be displayed, is
something that the CheckboxSelect2 should know about. Basically the
title logic in ApplicationContainer.jsx, now also pre-generates the
options props meant to passed to CheckboxSelect2 to render that
component, the CheckboxSelect2 logic is bleeding into the outer
container.
Let's look at the argument in the stackoverflow post Pass props to parent component in React.js and how it relates to this design problem and an analogy:
"The parent already has that child prop. ...  if the child has a
prop, then it is because its parent provided that prop to the
child!"  Sure, the ApplicationContainer has all the knowledge it
needs to generate the title for the parent DynamicDropdown based on
the checked states of the child CheckboxSelect2, but then is it
really the responsibility of the ApplicationContainer?
Let me give an analogy of a Manager asking an Employee to produce a Report. You can say, the Manager already has all the info, he can surely produce the Report himself! Having a controlled component where the
container manages update to the props for the child via callbacks, seems to me like a Manager passing a bucket to the Employee, the Employee passes back bits and pieces of information in the bucket, and tell the Manager to do the work to summarize things instead of the Employee producing a good summary (which is good encapsulation).
"Additionally, you could have used a ref directly on the child"
I think in the Choices 1) I stated above, using ref does not seem to work when you want up-to-date information from the child as
there is a circular dependency issue with one-way dataflow (ie.
parent needs to get up-to-date summary information from the child,
but the child first depends on the up-to-date information from the
parent).
If you have read this end-to-end and understood the problem, I appreciate your effort! Let me know what you think.

Resources