React - prevstate issues, getting back old value - reactjs

I have an SVG map with paths, and those paths change colors when I hover over them.
It changes state of specific section, for example my state looks like that:
POL3139: {
color: '#fbb9c5'
},
I am trying to switch back to the base color after I leave the path.
Here I am changing
onHover = (event) => {
event.stopPropagation();
const e = event.target.id
this.setState(prevState => ({
[e]: {
...prevState,
color: '#650df9'
},
}));
}
It totally works and changes my color to the picked one.
But then I am trying to revert back to the original one.
I tried that by making a base color in the state:
POL3139: {
color: '#fbb9c5',
base: '#fbb9c5'
},
and then onMouseLeave:
onLeave = (event) => {
event.stopPropagation();
const e = event.target.id;
this.setState(prevState => ({
[e]: {
...prevState,
// color: prevState.base - doesn't work
// color: prevState.[e].base - doesn't work
// color: [prevState.e.base] - doesn't work
color: 'pink'
}
}));
}
I was trying many possible solutions but I can't get it to work.
I am still learning react and it might be an easy one but I can't figure it out.

I don't think you're deconstructing your prevState properly.
Here's an example to illustrate how to deconstruct:
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
state = {
style: {
color: "black",
base: "black",
cursor: "pointer"
}
};
handleMouseEnter = e => {
const { style } = this.state;
this.setState({ style: { ...style, color: "red" } });
};
handleMouseLeave = e => {
const { style } = this.state;
this.setState({ style: { ...style, color: style.base } });
};
render() {
return (
<div
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={this.state.style}
>
<h1>hello</h1>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In this case style is equivalent to your [e].
In particular, look at the deconstructing here:
this.setState({ style: { ...style, color: style.base } });
There's a working example here.

Related

React material-ui (MUI) 5 conditional CSS classes

I'm moving from React material-ui 4 to MUI 5.
How do I achieve this type of pattern using the new styled API (or whatever makes sense)?
I'm using Typescript.
const useStyles = makeStyles(theme => ({
topBar: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: TRANSITION_DURATION,
}),
marginLeft: DRAWER_WIDTH,
},
topBarShift: {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: TRANSITION_DURATION,
}),
marginLeft: 0,
},
}));
function Header({ drawer }: IHeader) {
const classes = useStyles();
...
return (
<div className={`${classes.topBar} ${!drawer && classes.topBarShift}`}>
...
</div>
);
}
If I understand your question clearly, you just want a conditional string.
This can be done by creating a util function for reusability:
type ConditionalClasses = {
[className: string]: bool
}
const conditionalClass = (classes: ConditionalClass) =>
Object.keys(classes).reduce((combinedClassName, className) => classes[className] ? `${combinedClassName} ${className}` : combinedClassName, "")
Usage goes as follows
// Output: "a"
const newClassName = conditionalClasses({a: true, b: false})
Alternatively, you could use clsx
-- Edit
Looks like I misread and hence misunderstood the question.
If you want to use the styled API styles conditionally, you can use the overridesResolver option provided by the styled API.
const CustomDivComponent = styled("div", {
overridesResolver: (props, styles) => {
// Do your conditional logic here.
// Return the new style.
return {};
}
})(({ theme }) => ({
// Your original styles here...
}));
More documentation can be found here

How to implement react-dnd useDragLayer?

I have a component that currently uses the useDrag hook to connect to react-dnd. It works well, except for previews. I want to implement useDragLayer instead to see if it would help with my preview problems, as many online threads suggest.
This is my current (simplified) useDrag implementation:
const [{ isDragging }, connectDragSource, connectPreview] = useDrag({
item,
collect: monitor => ({
isDragging: monitor.getItem()?.index === item.index,
})
})
return (
<Wrapper ref={connectPreview} isDragging={isDragging}>
<DragHandle ref={connectDragSource} />
</Wrapper>
)
How do I use useDragLayer in this context, in a way that might help with my previews? The docs example makes little sense to me...
How do I connect my rendered components using useDragLayer api? useDragLayer doesn't return drag source and preview connector functions (like useDrag does on index 1 and 2 of the returned array), and its collect function doesn't provide a DragSourceConnector instance either. So what do I do with the hook/returned value after I call it?
I just resolved this and want to share it to help others :)
You will need to do couple of things for this to fully work.
Disable the default preview behavior by adding the following useEffect
import { getEmptyImage } from "react-dnd-html5-backend";
const [{ isDragging }, drag, dragPreview] = useDrag(() => ({
type: "BOX",
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}));
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
Create the custom default layer
export const CustomDragLayer = (props: {}) => {
const {
itemType,
isDragging,
initialCursorOffset,
initialFileOffset,
currentFileOffset,
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialCursorOffset: monitor.getInitialClientOffset(),
initialFileOffset: monitor.getInitialSourceClientOffset(),
currentFileOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}));
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(
initialCursorOffset,
initialFileOffset,
currentFileOffset
)}
>
<div>Your custom drag preview component logic here</div>
</div>
</div>
);
};
const layerStyles: CSSProperties = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
border: "10px solid red",
};
function getItemStyles(
initialCursorOffset: XYCoord | null,
initialOffset: XYCoord | null,
currentOffset: XYCoord | null
) {
if (!initialOffset || !currentOffset || !initialCursorOffset) {
return {
display: "none",
};
}
const x = initialCursorOffset?.x + (currentOffset.x - initialOffset.x);
const y = initialCursorOffset?.y + (currentOffset.y - initialOffset.y);
const transform = `translate(${x}px, ${y}px)`;
return {
transform,
WebkitTransform: transform,
background: "red",
width: "200px",
};
}
Add the <CustomDragLayer /> to the top-level component
You will need to include the ref={drag} to the component you want to drag and remove the connectPreview ref completely.
Hopefully, this helps you.

How to rerender value of child from HOC

I want to have a button submiting to stripe a specific quantity, based on an input that can be changed by typing or clicking an increment/decrease button. I have the increment/decrease functions on an higher order component but i am unable to submit it to the button of stripe (child from my understanding)
Relevant code below:
withCheckout (HOC):
import React from "react"
const UpdatedComponent = (OriginalComponent) => {
class NewComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 1
}
this.handleChange = this.handleChange.bind(this)
}
increment = () => {
this.setState(prevState => {
return { count: prevState.count + 1}
})
}
decrease = () => {
this.setState(prevState => {
return { count: prevState.count - 1 }
})
}
handleChange(event) {
this.setState({
count: parseInt(event.target.value)
});
}
render() {
return <OriginalComponent count={this.state.count} increment={this.increment} decrease={this.decrease} handleChange={this.handleChange} />
}
}
return NewComponent
}
export default UpdatedComponent
Checkout:
import React, { useState } from "react"
import { loadStripe } from "#stripe/stripe-js"
import UpdatedComponent from "./withCheckout"
const buttonStyles = {
fontSize: "13px",
textAlign: "center",
color: "#000",
padding: "12px 60px",
boxShadow: "2px 5px 10px rgba(0,0,0,.1)",
backgroundColor: "rgb(255, 178, 56)",
borderRadius: "6px",
letterSpacing: "0.2ch",
display: "block",
margin: "1.5em auto 1.5em auto",
}
const buttonDisabledStyles = {
opacity: "0.5",
cursor: "not-allowed",
}
let stripePromise
const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe("KEY_HERE")
}
return stripePromise
}
const Checkout = props => {
const [loading, setLoading] = useState(false)
const redirectToCheckout = async event => {
event.preventDefault()
setLoading(true)
const stripe = await getStripe()
const { error } = await stripe.redirectToCheckout({
mode: "payment",
lineItems: [{ price: "PRICE_ID_HERE", quantity: props.count }],
successUrl: `http://localhost:8000/page-2/`,
cancelUrl: `http://localhost:8000/`,
shippingAddressCollection: {
allowedCountries: ['PT'],
}
})
if (error) {
console.warn("Error:", error)
setLoading(false)
}
}
return (
<button
disabled={loading}
style={
loading ? { ...buttonStyles, ...buttonDisabledStyles } : buttonStyles
}
onClick={redirectToCheckout}
>
Comprar {props.count}
</button>
)
}
export default UpdatedComponent(Checkout)
Example:
See this image
When I change the input to 7, I expect the button text to be "Comprar 7" and I expect to submit quantity: 7 in the striperedirect function. I think the problem has to do with the way props are set, as my Counter is working well the props were passed as const {count} = this.props. Should I add the Counter.js code as well?

How do you access the Material UI theme inside event handlers?

I have event handlers for things like onClick or onFocus, and I can't figure out how to use the theme inside of the handler code. I want to change the color of an iconButton and I don't want to hard-code the color because we want components that can be general use, and eventually work with themes using completely different colors.
Tried using withTheme in addition to withStyles, so I can get the theme inside of the render(), but I can't get to it from a handler called from that rendering. Tried passing it, calling as a prop, declaring constants based upon theme values in the class (both inside and outside of render), nothing.
I don't know if this is possible, or not built in, or what. I'm hoping that I'm just missing something.
Environment: CodeSandBox, so CreateReactApp. Material-UI plus React-Select, withStyles and withTheme (useTheme help here?).
handleInfoClick = (e) => {
if (this.instructionsContent.current.style.display !== "block") {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = "#f9be00"; //works
} else {
this.instructionsContent.current.style.display = "none";
this.instructionsButton.current.style.color = this.theme.palette.text.disabled; // doesn't work
also tried this:
handleSelectFocus = () => {
if (this.state.visited === false) {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = this.activeButtonColor;
this.setState({ visited: true });
}
};
...
render() {
const { theme } = this.props;
...
const activeButtonColor = theme.palette.secondary.main;
Finally, also tried to use the classes I can use within render(), but it doesn't recognize those either:
const styles = theme => ({
...
infoButton: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.text.disabled,
"&:active": {
color: theme.palette.secondary.main
}
},
infoButtonActive: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.secondary.main
},
....
Hoping one of these approaches would give me a color for my <IconButton> - from my theme:
<div className={classes.infoButtonDiv}>
<IconButton
aria-label="Instructions"
className={classes.infoButton}
buttonRef={this.instructionsButton}
onClick={this.handleInfoClick}
>
<HelpOutline />
</IconButton>
</div>
(in a different theme.js file applied to the root element:
const theme = createMuiTheme({
typography: {
fontFamily: ["Roboto", '"Helvetica Neue"', "Arial", "sans-serif"].join(",")
},
palette: {
primary: {
main: "#00665e"
},
secondary: {
main: "#f9be00"
}
},
overrides: {
LeftNav: {
drawerDiv: {
backgroundColor: "#00665e",
width: 300
}
}
},
direction: "ltr",
typography: {
useNextVariants: true
}
});
Triggering a state change onClick will update the color, but only if you pass one of the supported values for the IconButton color prop ("primary" or "secondary").
import React, { Component } from "react";
import IconButton from "#material-ui/core/IconButton";
import DeleteIcon from "#material-ui/icons/Delete";
class ButtonStyle extends Component {
constructor(props) {
super(props);
this.state = {
buttonColor: "primary"
};
}
handleClick = e => {
this.setState({
buttonColor: "secondary"
});
};
render() {
const buttonColor = this.state.buttonColor;
return (
<div>
<IconButton
aria-label="Delete"
color={buttonColor}
onClick={this.handleClick}
>
<DeleteIcon />
</IconButton>
</div>
);
}
}
export default ButtonStyle;

How to test if styles are dynamically applied on a React component

I've written a React component, Button:
import PropTypes from 'prop-types'
import Radium from 'radium'
import React from 'react'
import { Icon } from 'components'
import { COLOURS, GLOBAL_STYLES, ICONS, MEASUREMENTS } from 'app-constants'
#Radium
export default class Button extends React.Component {
static propTypes = {
children: PropTypes.string,
dark: PropTypes.bool,
icon: PropTypes.oneOf(Object.values(ICONS)).isRequired,
style: PropTypes.object,
}
render() {
const { children, dark, icon, style } = this.props
let mergedStyles = Object.assign({}, styles.base, style)
if (!children)
mergedStyles.icon.left = 0
if (dark)
mergedStyles = Object.assign(mergedStyles, styles.dark)
return (
<button
className="btn btn-secondary"
style={mergedStyles}
tabIndex={-1}>
<Icon name={icon} style={mergedStyles.icon} />
{children &&
<span style={mergedStyles.text}>{children}</span>
}
</button>
)
}
}
export const styles = {
base: {
backgroundColor: COLOURS.WHITE,
border: `1px solid ${COLOURS.BORDER_LIGHT}`,
borderRadius: GLOBAL_STYLES.BORDER_RADIUS,
cursor: 'pointer',
padding: GLOBAL_STYLES.BUTTON_PADDING,
':focus': {
outline: 'none',
},
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW,
},
icon: {
fontSize: GLOBAL_STYLES.ICON_SIZE_TINY,
left: '-3px',
verticalAlign: 'middle',
},
text: {
fontSize: GLOBAL_STYLES.FONT_SIZE_TINY,
fontWeight: GLOBAL_STYLES.FONT_2_WEIGHT_MEDIUM,
marginLeft: `${MEASUREMENTS.BUTTON_PADDING.HORIZONTAL}px`,
verticalAlign: 'middle',
},
},
dark: {
backgroundColor: COLOURS.PRIMARY_3,
border: `1px solid ${COLOURS.PRIMARY_2}`,
color: COLOURS.WHITE,
':hover': {
boxShadow: GLOBAL_STYLES.BOX_SHADOW_DARK,
},
},
}
I've also written a test for Button with Jest and Enzyme, which validates if its dark styles are applied when its dark prop is set to true:
import { ICONS } from 'app-constants'
import Button, { styles } from 'components/Button'
describe("<Button>", () => {
let props
let mountedComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
mountedComponent = undefined
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const componentStyles = getComponent().props().style
expect(componentStyles).toEqual(expect.objectContaining(styles.dark))
})
})
})
As you can see, I do this by checking if the properties of styles.dark are inside the rendered Button's style attribute. If they are, then it means the styles have applied successfully.
The issue is that styles.dark and componentStyles don't match:
Output of console.log(styles.dark)
ObjectContaining{
":hover": {
"boxShadow": "0px 0px 0px 2px rgba(0,0,0,0.2)"
},
"backgroundColor": [Object],
"border": "1px solid rgb(47, 52, 63)",
"color": [Object]
}
Output of console.log(componentStyles)
{
"backgroundColor": "rgb(31, 34, 40)",
"border": "1px solid rgb(47, 52, 63)",
"borderRadius": "4px",
"color": "rgb(255, 255, 255)",
"cursor": "pointer",
"padding": "3px 5px 3px 5px"
}
I notice a few issues here:
styles.dark has several Color() [Object]s from the color library. They haven't outputted their rgb() value as a string, but the same properties in componentStyles have, thus resulting in a mismatch.
componentStyles has Radium's interactive styles stripped, such as :focus and :hover (I assume Radium does this during rendering triggered by Enzyme's shallow() function). This causes a mismatch with styles.dark, which doesn't have these properties stripped.
As a result, I'm not sure how to test this. I can't think of any alternative solutions to validate that styles.dark has been applied. I think that doing the following to styles.dark during testing would be a solution:
Recursively cause all Color() [Object]s to process so they output their rgb() value as a string.
Recursively remove all interactive Radium styles (like :focus and :hover)
Doing so would cause styles.dark to equal the value of componentStyles, thus passing the test. I'm just not sure how to do it.
I came back to this a few days later with fresh eyes and thought of a solution:
describe("<Button>", () => {
let props
let mountedComponent
let defaultComponent
const getComponent = () => {
if (!mountedComponent)
mountedComponent = shallow(
<Button {...props} />
)
return mountedComponent
}
beforeEach(() => {
props = {
children: undefined,
dark: undefined,
icon: ICONS.VIBE,
style: undefined,
}
defaultComponent = getComponent()
mountedComponent = undefined
})
describe("when `dark` is `true`", () => {
beforeEach(() => {
props.dark = true
})
it("applies the component's `dark` styles", () => {
const darkStyles = getComponent().props().style
expect(defaultComponent.props().style).not.toEqual(darkStyles)
})
})
})
Rather than asserting that the rendered component's style prop contains the styles.dark (which is brittle), it just checks to see if the styles have changed at all when the dark prop is set to true.

Resources