Call to setState in onDragStart causes DOM node to be deleted? - reactjs

I use a list of styled components for displaying some info. I want this info to be sortable. The real problem I'm trying to solve is actually way more complex than what I'm demonstrating here. So any odd design choices are very specific to what I'm trying to do. I'm just mentioning it because the code I'm showing will be very simplified but it will also show some of these at first glance odd design choices.
I've read this article: https://medium.com/the-andela-way/react-drag-and-drop-7411d14894b9
Temitope Emmanuel (the author) did what I'm trying to achieve but with just a plain div. I don't know whether he tested all of what he proposes in his article.
Off to some code:
import React, { Component, Fragment } from 'react';
import styled from 'styled-components';
export default class SomeList extends Component {
constructor(props) {
super(props);
// in real problem all of these are props
// pulled off the state of a parent
this.state = {
dragging: false,
listOfChildrenInOrder: ['1', '2', '3'],
itemComponent: styled.div`
border: 1px solid black;
`,
};
}
render() {
const {
dragging,
listOfChildrenInOrder,
itemComponent: ItemComponent,
} = this.state;
const {
children,
} = this.props;
const Container = styled.div`
display: grid;
grid-template-rows: max-content;
grid-template-columns: repeat(${listOfChildrenInOrder.length}, max-content) 1fr;
`;
const Droppable = styled.div`
&:hover {
background-color: rgba(0,0,0,0.4);
}
`;
return (
<Container>
<Fragment>
{listOfChildrenInOrder.map(((cid, i) => (
<ItemComponent
draggable
key={`ic-${cid}`}
style={{
gridArea: `1 / ${i + 1} / span 1 / span 1`,
}}
onDragStart={(e) => {
this.setState({ dragging: true });
e.dataTransfer.setData('text/plain', `${cid}`);
}}
onDragEnd={() => {
this.setState({ dragging: false });
// doesn't even fire anymore
}}
>
{children.find(c => c.key === cid)}
</ItemComponent>
)))}
</Fragment>
<Fragment>
{dragging && listOfChildrenInOrder.map(((cid, i) => (
<Droppable
key={`d-${cid}`}
style={{
gridArea: `1 / ${i + 1} / span 1 / span 1`,
}}
onDragOver={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onDrop={() => {
// do whatever (out of scope), doesn't get called anyway
}}
>
{children.find(c => c.key === cid)}
</Droppable>
)))}
</Fragment>
</Container>
);
}
}
I'm expecting the reconciler (Fiber) to update the DOM node without straight out replacing it in the middle of a drag operation. I'm using these things to act as highlighters. The real Problem I'm trying to solve actually makes a difference on where exactly stuff gets dropped, so the grid in the real problem is finer, with more droppables and one item component spaning multiple grid columns. Like I said: odd choices, but not without purpose.

Okay, I know now what was causing this whole operation to fail. The reason was dynamically creating new styled components in the render loop. Never do that. Just another rule of thumb learned.

Related

React: prevent list from rerendering all elements on prop change

I'm trying to recreate the effect shown at https://hexed.it/
When you hover over either list the corresponding byte in the other list is also highlighted. I figured a panel with each list inside it that had a state with the current hovered byte would do but it seems that React wants to re-render the entire list or do something strange every time resulting in larger files being unbearably slow.
I see a lot of "use memo! use the useCallback hook!" when searching and I've tried... it's still slow and I'm not sure why. It seems like it's only rendering the updated HexByte but it's still unacceptably slow for large files.
Sandbox: https://codesandbox.io/s/flamboyant-ellis-btfk5s
Can someone help me quicken/smooth out the hovering?
I solved it using this answer: Prevent DOM element re-render in virtualized tree component using react-window
In short the things I've learned:
memo has no effect if a component has a useState in it
Large lists of data should be rendered using a library like react-window
The cell rendering function as mentioned in the answer above can't be part of a parent component
As an example for anyone coming here, the new HexPanel class looks like so
import Box from '#mui/material/Box';
import { memo } from 'react';
import { FixedSizeGrid as Grid, areEqual } from 'react-window';
const HexByte = memo(function HexByte(props) {
const onMouseEnter = () => {
props.onHover(props.index);
//setInside(true);
}
const onMouseLeave = () => {
//setInside(false);
}
const onClick = () => {
//setClicked(true);
}
return (
<span
style={{
display: 'inline-block',
padding: '5px',
backgroundColor: props.hoverIndex == props.index ? '#777' : 'transparent',
color: 'darkblue'
}}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{props.byte}
</span>
)
}, (prevProps, nextProps) => nextProps.hoverIndex != nextProps.index);
const Cell = memo(function({ data, columnIndex, rowIndex, style }) {
return (
<div style={style}>
<HexByte byte={data.hex[rowIndex][columnIndex]} onHover={data.onHover} hoverIndex={data.hoverIndex} index={`${rowIndex}${columnIndex}`} />
</div>
)
}, areEqual);
const HexPanel = (props) => {
return (
<Box
sx={{
fontFamily: 'Source Code Pro',
display: 'flex',
flexDirection: 'column',
}}
>
<Grid
columnCount={16}
columnWidth={30}
height={900}
itemData={props}
rowCount={props.hex.length}
rowHeight={35}
width={500}
>
{Cell}
</Grid>
</Box>
)
}
export default HexPanel;

Material UI Rating wrapping long values

I am currently trying to work around the Material UI rating component and how to do a flex-wrap if the icons overflow the width of the parent component.
If I try to add flex-wrap: wrap to the rating component, it actually wraps the icons but the interactive functionality stops working pas the first line.
Here is a code example below to better demonstrate this:
Code Example in CodeSandbox
Is there a way to make it work with flex-wrap? If anyone could help I will very much appreciate.
I have decided that was better to build one by myself with the ability to wrap if the max value is big.
Will leave it here so someone who might have the same issue as me can use it.
CustomRating.js
import React, { useState } from 'react'
import { Tooltip } from '#mui/material'
import './CustomRating.css'
function CustomRating({ max, value, onChange, icon, emptyIcon }) {
const [innerValue, setInnerValue] = useState(value)
const checkIfIconInsideValue = (index) => {
return value >= index + 1
}
const handleMouseHover = (e, index) => {
if (e.type === 'mouseenter') {
setInnerValue(index)
return
}
setInnerValue(value - 1)
}
return (
<Tooltip title={innerValue} placement='top'>
<div className='custom-rating-main-div'>
{Array.from({ length: max }).map((elem, index) => {
return (
<div
className={`custom-rating-icon-div ${checkIfIconInsideValue(index) ? 'filled' : ''}`}
key={index}
onClick={() => onChange(index + 1)}
onMouseEnter={(e) => handleMouseHover(e, index)}
onMouseLeave={(e) => handleMouseHover(e, index)}
>
{checkIfIconInsideValue(index) || innerValue >= index ? icon : emptyIcon}
</div>
)
})}
</div>
</Tooltip>
)
}
export default CustomRating
CustomRating.css
.custom-rating-main-div {
display: flex;
flex-wrap: wrap;
}
.custom-rating-icon-div {
cursor: pointer;
}
.custom-rating-icon-div.filled > svg {
fill: #61634f
}
.custom-rating-icon-div > svg {
fill: rgba(97, 99, 79, 0.5)
}
.custom-rating-icon-div:hover > svg {
fill: #61634f;
transform: scale(1.2);
}
As you may notice this is specific to my problem but can be very easily adapted to any case.
keep in mind that this is very rough and can be updated to better follow conventions and for better performance, but for now it is my solution

How to pass an onClick to parent in React

I have a React component called beercard which populates an item for me and adds a like button, here's the Beercard file:
function Beercard(props) {
const [active, setActive] = useState(false);
const changeLike = () => {
setActive(!active)
}
return (
<BeerCard>
<Content>
<Lockup text={props.description} tag="h3" title={props.title}/>
</Content>
<ImagContainer>
<Like liked={active} />
<Image url={props.image}/>
</ImagContainer>
</BeerCard>
)
}
Then I have the Like component, which I will use to handle the onClick:
const Icon = styled.svg`
width: 32px;
height: 32px;
stroke: white;
stroke-width: 2px;
fill: ${props => props.liked ? 'white' : 'transparent'};
`
const like = (props) => {
return (
<LikeButton onClick={props.action}>
<Icon liked={props.liked}>
<path
id="heart-icon"
d="M16,28.261c0,0-14-7.926-14-17.046c0-9.356,13.159-10.399,14-0.454c1.011-9.938,14-8.903,14,0.454
C30,20.335,16,28.261,16,28.261z"
/>
</Icon>
</LikeButton>
)
}
It all works fine ie with the css for filled and unfilled, then if I set the state to true in the beercard file it sets the heart to active/fills it. However the onClick function is not doing anything and I'm not sure how to communicate between the two files to add this event. It's a simple on/off toggle with true and false but I don't know how to handle the event, can somebody see what I need to do?
If I understand correctly, you just need to pass along the desired function as a property to the Like component. You're actually 90% there, but you're calling props.action in onClick, but passing nothing to the action property.
Since you already have everything, you can change the line where you use your Like component to:
<Like liked={active} action={changeLike} />

CSS styling for a boolean React hook button

I have a simple message board with nested comments built in React. I'm trying to add a favorite button (just a button that toggles filled/unfilled contingent on its boolean value). Every tutorial I find involves making a new component, but I'd like to include this in my primary app code (using a hook/useState).
I've tried some various CSS stuff like active, etc. I'm rusty with CSS and a bit lost using it on JSX. Ideally, it'd be a star button that fills/unfills, but I want to figure out the simple basics here first.
function Toggle(props) {
const [toggleState, setToggleState] = useState(false);
function toggle() {
setToggleState(toggleState === false? true : false);
}
return (
<div {...props}>
<Button
className={`switch ${toggleState}`}
onClick={toggle}>
Favorite
</Button>
</div>
)
}
Toggle=styled(Toggle)`
//dunno what to put here
The button shows up just fine but nothing I try in the style will make it toggle colors (or filled/unfilled) on click. How can I do this?
First of all this syntax is more readable
function toggle() {
setToggleState(!toggleState);
}
then you can use this to change the class of your button
className={`switch ${toggleState ? "some_class" : "some_other_class"}`}
If you use styled-components, you can customize Button directly. In Tagged templates, switch styles according to prop.
Demo
const Button = styled(Icon)`
color: ${props => (props.filled ? 'pink' : 'transparent')};
-webkit-text-stroke: 2px pink;
overflow: hidden;
margin: 0 0.5em;
cursor: pointer;
`
function Toggle(props) {
const [toggleState, setToggleState] = useState(false)
function toggle() {
setToggleState(toggleState => !toggleState)
}
return (
<Button filled={toggleState} onClick={toggle}>
favorite
</Button>
)
}
Adapting based on props
Following your code:
const StyledToggle=styled(Toggle)`
&.true {
//true styles
}
&.false {
//false styles
}
`;
As you are using css in js you could also pass the state as a prop to the component and read it inside the tagged template:
return (
<div {...props}>
<Button
toggleState={toggleState}
onClick={toggle}>
Favorite
</Button>
</div>
)
}
const StyledToggle = styled(Toggle)`
font-weight: ${({toggleState}) => toggleState && "bold"};
`;

Nested Match routes with React Router v4 and MatchWithFade

This question is a follow-up to this:
Trouble with React Router v4 and MatchWithFade
I've got another (potentially silly) question about using MatchWithFade and React Router v4. What I'd like to do is have nested routes, so a top-level component might have this:
<MatchWithFade pattern='/one' component={One} />
...and then One might have this:
<Match pattern='/one/one' Component={OneOne} />
This doesn't strike me as an unusual pattern (though maybe it is). In any event, the behavior I'm observing is that, using the above example, if I load OneOne, it gets mounted, and then componentWillUnmount is immediately called. Had I to guess, I'd say that TransitionMotion is keeping track of a (perhaps hidden) instance of OneOne, and once the transition is complete, it unmounts that hidden component. As far as the basic UI is concerned, OneOne is rendered. However, if componentWillUnmount does any cleanup (like, say, removing something from Redux), then of course that action is fired, and any data tied to OneOne is blown away.
Here's a complete example that illustrates the problem:
import React, { Component } from 'react';
import BrowserRouter from 'react-router/BrowserRouter'
import { TransitionMotion, spring } from 'react-motion'
import Match from 'react-router/Match'
import Link from 'react-router/Link';
const styles = {
fill: { position: 'absolute', top: 0, left: 0 }
};
const MatchWithFade = ({ component:Component, ...rest }) => {
const willLeave = () => ({ zIndex: 1, opacity: spring(0) })
return (
<Match {...rest} children={({ matched, ...props }) => {
return (
<TransitionMotion
willLeave={willLeave}
styles={matched ? [ {
key: props.location.pathname,
style: { opacity: 1 },
data: props
} ] : []}
>
{interpolatedStyles => {
return (
<div>
{interpolatedStyles.map(config => (
<div
key={config.key}
style={{...styles.fill, ...config.style }}>
<Component {...config.data}/>
</div>
))}
</div>
)
}}
</TransitionMotion>
)
}}/>
)
}
const TwoOne = () => {
return (
<div>Two One</div>
)
}
class TwoTwo extends Component {
componentWillUnmount() {
console.log("TwoTwo will unmount")
}
render () {
return (
<div>Two Two</div>
)
}
}
const TwoHome = () => {
return (
<div>Two Home</div>
)
}
class One extends Component {
componentWillUnmount () {
console.log("ONE UNMOUNTING")
}
render () {
return (
<div style={{ width: 300, border: '1px solid black', backgroundColor: 'orange', minHeight: 200}}>
One one one one one one one one one one<br />
One one one one one one one one one one<br />
</div>
)
}
}
const Two = () => {
return (
<div style={{ width: 300, border: '1px solid black', backgroundColor: 'yellow', minHeight: 200}}>
<Match pattern='/two/one' component={TwoOne} />
<Match pattern='/two/two' component={TwoTwo} />
<Match pattern='/two(/)?' exactly={true} component={TwoHome} />
</div>
)
}
class App extends Component {
render () {
return (
<BrowserRouter>
<div style={{padding: 12}}>
<div style={{marginBottom: 12}}>
<Link to='/one'>One</Link> || <Link to='/two'>Two</Link>
|| <Link to='/two/one'>Two One</Link>
|| <Link to='/two/two'>Two Two</Link>
</div>
<div style={{position: 'relative'}}>
<MatchWithFade pattern='/one' component={One} />
<MatchWithFade pattern='/two' component={Two} />
</div>
</div>
</BrowserRouter>
)
}
}
export default App;
If you load this and open a console, toggle between the One and Two links. You'll see the cross fade happen in the UI, and you'll see "ONE UNMOUNTING" in the console when the transition from One to Two completes. So that's right.
Now, click between Two One and Two Two. In this case, when Two One is clicked, you'll immediately see "TwoTwo will unmount" in the console, which is good. However, if you click Two Two, you'll see "TwoTwo will unmount" after about a second--which I take to be the amount of time the parent MatchWithFade takes to execute.
So I'm not sure what's going on here. Is my code just busted? Am I doing something that RRv4 cannot support? Have I uncovered a bug?
Any help/guidance is appreciated!
Your issue is your use of props.location.pathname as a key. This should always be the same for a component, but the way that you have written it, it changes each time that you navigate. Try changing this:
const styles = {
fill: { position: 'absolute', top: 0, left: 0 }
};
to:
const styles = {
fill: { position: 'relative', top: 0, left: 0 }
};
and you will see that you are rendering two instances of <Two> (one for each key).
If you were to use a constant key, such as rest.pattern (the pattern associated with this <Match>), your issue would go away.
styles={matched ? [ {
key: rest.pattern,
style: { opacity: 1 },
data: props
} ] : []}

Resources