Nested Match routes with React Router v4 and MatchWithFade - reactjs

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
} ] : []}

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;

React: Slider with useEffect hook not working properly when trying to save the slider's state into local storage

I'm trying to save the state of my slider in local storage using the UseEffect hook, so the selected value in the slider doesn't get lost when the user refreshes the page. However, the slider is acting weirdly when I add this functionality.
Here is the code in the parent component:
function ParentComponent() {
const [prefState, setPrefState] = React.useState(50);
React.useEffect(() => {
const data = window.localStorage.getItem("pref_state");
if (data) {
setPrefState(JSON.parse(data));
}
}, []);
React.useEffect(() => {
window.localStorage.setItem("pref_state", JSON.stringify(prefState));
});
const handlePrefSliderChange = (event, new_value) => {
setPrefState(new_value);
};
return (
<div className={classes.root}>
<Grid item xs={12} >
<CustomizedPrefSlider
PrefSliderValues={prefState}
onPrefSliderChange={handlePrefSliderChange}
/>
</Grid>
</div>
);
}
export default ParentComponent;
Here is the code for the child component:
const PrefSlider = withStyles({
root: {
color: '#52af77',
height: 8,
padding: '13px 0',
},
track: {
height: 8,
},
rail: {
color: '#d8d8d8',
opacity: 1,
height: 8,
},
})(Slider);
export default function CustomizedPrefSlider({PrefSliderValues, onPrefSliderChange}) {
const classes = useStyles();
return (
<div>
<div className={classes.title}>
<Typography className={classes.titleFont}>
How important is this feature to you?
</Typography>
</div>
<div className={classes.root}>
<PrefSlider
aria-labelledby="discrete-slider"
value={PrefSliderValues}
onChange={onPrefSliderChange}
min={0}
max={100}
step={25}
/>
</div>
</div>
);
}
Is this the correct way of saving the state of the slider to avoid losing data when the user refreshes the page? Why am I missing some of the stylings that I added to my slider when I add this functionality?
Thanks in advance!
Doing it this way looks like it should technically work, but seems very inefficient.
I'd suggest using a custom made hook, something like useLocalStorage or #rehooks/local-storage
useLocalStorage is nice because the code is small and easy to understand, and it would shrink your code down to this:
const [prefState, setPrefState] = useLocalStorage(50);

React-testing-library not rendering computed styles from stylesheet

Basic scenario is such: I have a component which has width: 100% as defined in a stylesheet. Therefore it should retain the width of its parent component. I want to calculate the width of my component and apply it to my child component because I am rendering it via createPortal and I would like them to be the same width. This works in the browser. However, in my test, I am finding that window.getComputedStyle(component) is not returning any of the styles applied from the stylesheet.
As suggested, I could mock the javascript window, but that's actually counter to what I'm hoping to do, I think. I want to verify the behavior that is present in the browser, that window.getComputedStyle() returns all styles applied, not just the inline styles.
I have put a simple example into a codesandbox: https://codesandbox.io/s/goofy-wilson-6v4dp
Also here:
function App() {
return (
<div className="App">
<WidthComponent />
</div>
)
}
function WidthComponent() {
const myInput = useRef();
const [inputWidth, setInputWidth] = useState(0);
useEffect(() => {
console.log("in handleLoad");
const width = myInput.current ? myInput.current.offsetWidth : 0;
setInputWidth(width);
}, [myInput]);
return (
<div className="inherited-width" ref={myInput}>
<div style={{ width: inputWidth }} className="child-element">
Hello
</div>
</div>
);
}
// test
test("width is inherited", () => {
const { rerender } = render(
<div style={{ width: "452px" }}>
<WidthComponent />
</div>
);
const element = document.getElementsByClassName("child-element").item(0);
rerender(
<div style={{ width: "452px" }}>
<WidthComponent />
</div>
);
expect(window.getComputedStyle(element).width).toBe("452px");
});
.App {
font-family: sans-serif;
text-align: center;
width: 500px;
}
.inherited-width {
width: inherit;
}
Any help is appreciated.
However, in my test, I am finding that window.getComputedStyle(component) is not returning any of the styles applied from the stylesheet.
Note that if you're running your tests in JSDOM (i.e. every Jest test) then CSS isn't fully implemented. Specifically, the cascade part of CSS is not implemented (https://github.com/jsdom/jsdom/pull/2690). Inheritance is only partially implemented (display and visibility) (https://github.com/jsdom/jsdom/issues/2160).
I would suggest running tests that assert on computed styles only in browsers, not JSDOM. A codesandbox test is not running in an actual browser environment.

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

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.

Trouble with React Router v4 and MatchWithFade

This question is related to this post:
React Router v4 Match transitions using React Motion
...but I thought it deserved its own question.
I'm trying to figure out how to use the <MatchWithFade> example taken from here:
https://react-router.now.sh/animated-transitions
The problem I'm seeing is that if I have two tabs, and I want to fade between them, I'm only seeing the fade effect on one of the two tabs, and it depends on which <MatchWithFade> appears first in my code.
The relevant code is as follows:
const One = () => {
return (
<div style={{position:'absolute', top: 0, left: 0, width: 300, backgroundColor: 'orange'}}>
One one one one one one one one one one
</div>
)
}
const Two = () => {
return (
<div style={{position:'absolute', top: 0, left: 0, width: 300, backgroundColor: 'yellow'}}>
Two two two two two two two two two two
</div>
)
}
class AppBody extends Component {
render () {
return (
<div style={{position: 'relative'}}>
<MatchWithFade pattern='/one' component={One} />
<MatchWithFade pattern='/two' component={Two} />
</div>
)
}
}
In this example, navigating to /one, (using the React Router <Link> component) will cause the fade to happen, but if I navigate to /two, there is no fade. Then, if I list <MatchWithFade pattern='/two' ... /> first, then I see the fade transition to /two, but not /one.
Just using <Match> works fine, so I don't think it's a fundamental issue with how I have <BrowserRouter> configured.
I'm hoping that I'm just doing something silly, but for the life of me, I can't figure out what. Any guidance is appreciated.
UPDATE
I couldn't figure out how to made a jsbin using React Router (couldn't figure out how to reference the methods and objects on it, since I've only ever used RR via import statements). So here's the next best thing: this is a complete example that demonstrates this issue:
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 MatchWithFade = ({ component:Component, ...rest }) => {
const willLeave = () => ({ 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={{...config.style }}
>
<Component {...config.data}/>
</div>
))}
</div>
)
}}
</TransitionMotion>
)
}}/>
)
}
const One = () => {
return (
<div style={{position:'absolute', top: 0, left: 0, 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={{position:'absolute', top: 0, left: 0, width: 300, border: '1px solid black', backgroundColor: 'yellow', minHeight: 200}}>
Two two two two two two two two two two<br />
Two two two two two two two two two two<br />
</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>
</div>
<div style={{position: 'relative'}}>
<MatchWithFade pattern='/one' component={One} />
<MatchWithFade pattern='/two' component={Two} />
</div>
</div>
</BrowserRouter>
)
}
}
export default App;
There are only very minor differences between this MatchWithFade, and the one taken from the React Router docs. The biggest difference is that I pulled out the zIndex reference, but that did not affect the behavior.
If it's relevant, I started this project using create-react-app, and I'm using React Router version 4.0.0-alpha.6.
This is an issue with the style you're applying (or not) from the MatchWithFade example. Add zIndex: 1 back to your willLeave function, as this ensures the outgoing route is over top of the incoming in order to see the opacity fade.
Then add the absolute positioning back to the wrapper div you're applying the style to (styles.fill in the website example) so that they can overlap each other.
Here is a gist with your code fixed up.

Resources