I have a NextJS app with two very different layouts depending on whether it's in landscape or portrait mode.
My parent page is effectively:
<NavWrapper> <MyPage> </NavWrapper>
The NavWrapper component takes care of the portrait v landscape layouts, and passes {children} into two different components. Each of those components provides the layout, and a container, into which the actual page is rendered.
size.isPortrait?
<NavPortrait children={props.children}/> :
<NavLandscape children={props.children}/>
No the problem I'm having is this - Inside 'MyPage' I have a showModal=useState(), which I use to show a pop-up modal. This is working fine. But if I rotate the screen with the modal showing, it disappears. And stays disappeared if I rotate back.
I'm assuming that the change in orientation is causing a fresh instance of 'MyPage' which obviously has the showModal state to the default 'false'.
What's the best way to fix this? Should I have a higher level 'Modal' context? Should I somehow memoise 'MyPage'?
You'll likely need to store the state of showModal at a level higher than MyPage. The code sample you posted indicates that you're rendering a brand new component based on the value of size.isPortrait. You should store the important state at a higher level than your conditional component, and then pass it into the component (either through React Context, props, or some other way.)
If your components do not share any kind of state (or if they do not inherit their state from a singular source), they will always store their state independently.
const [showModal, setShouldShowModal] = useState(false);
return (
size.isPortrait
?
<NavPortrait children={props.children} showModal={showModal} />
:
<NavLandscape children={props.children} showModal={showModal} />
);
Related
I have a React application using Material UI with a component (which we can call DatePicker) shown below, sneakily changed for demo purposes.
Material UI animates clicks and other interactions with its components. When clicking a radio button that has already been selected, or a "time button" which doesn't change state, this animation is visible above. However, when such a click changes the state, the animation get interrupted.
I can see why this happens from a technical perspective; the DatePicker component calls setMinutes, which is a property passed in from its parent (where the state lives). This is a React.useState variable, which then updates its corresponding minutes variable. Minutes is then passed into DatePicker, which re-renders due to a prop change.
If state lived within DatePicker then this problem shouldn't rear its head; however, DatePicker is one part of a much larger form which dictates the contents of a table in the parent. To generate rows for this table, the parent must have this information.
Below is a sample reconstruction of the parent:
const Parent = () => {
const [minutes, setMinutes] = React.useState(15);
const [radioOption, setRadioOption] = React.useState('Thank You');
// Many other state variables here to hold other filter information
return (<div>
<DatePicker minutes={minutes} setMinutes={setMinutes} radioOption={radioOption} setRadioOption={setRadioOption}/>
</div>);
};
And here a sample reconstruction of DatePicker:
const DatePicker: React.FC<DatePickerProps> = props => {
const {minutes, setMinutes, radioOption, setRadioOption} = props;
return (<div>
<Radios value={radioOption} onChange={val => setRadioOption(val)}/>
<Minutes value={minutes} onChange{val => setMinutes(val)}/>
</div>);
};
I'm not sure what the best practice is in this situation, but I get the distinct feeling that this is not it. Does anyone have any advice? Thanks in advance!
Thank you for your comment, Ryan Cogswell. I did create a code sandbox, and found that the problem was not about React state management as much as what I was doing beyond what I provided in my question.
I was using the withStyles HOC to wrap my component, in a way similar to const StyledDatePicker = withStyles(styles)(DatePicker). I then used that styled element and put properties (minutes, etc) on that.
It turns out that using the unstyled DatePicker resolves this issue. I troubleshooted this further, and found that I had created the "Styled" component within the "render" method of the parent, meaning every time a prop change was pushed up the chain, the parent would re-render and the entire "Styled" component type would be created again (or so I believe). This would break reference integrity, which explains the "drop and recreate" behaviour.
This teaches the valuable lesson of keeping components small and using code sandboxes for troubleshooting. Thanks again!
For anyone interested, here is the Code Sandbox used for testing.
I have a component where I want to render different components based on screen size. If I reload the page while on mobile view, everything is ok, NavBarMobile is rendered and NavbarDesktop is not.
If I reload the page while on desktop view, then my NavbarMobile is rendered again instead of NavBarDesktop.
If I start resizing the screen to mobile and back to desktop view, NavBarDesktop is rendered correctly.
So, the problem is first page load while in Desktop view, how to fix that?
const { mainAppComponents, } = this.props
const { visible, } = this.state
return (
<Fragment>
<Responsive maxWidth={767}>
<NavBarMobile
onPusherClick={this.handlePusher}
onToggle={this.handleToggle}
rightItems={rightItems}
visible={visible}
>
{mainAppComponents.header}
{mainAppComponents.routes}
</NavBarMobile>
</Responsive>
<Responsive minWidth={768}>
<NavBarDesktop rightItems={rightItems}>{mainAppComponents.header}</NavBarDesktop>
{mainAppComponents.routes}
</Responsive>
</Fragment>
)
Igor-Vuk, I put together a quick codesandbox example just to make sure there was not a problem with how you are trying to implement the min/max width props. As you can see from this example, they do in fact work as expected. https://codesandbox.io/s/98pk46l7vr
Without seeing the rest of your component, or application, the issue may be due to something in your router. I'd recommend trying to remove some of the other components you are returning as children of the Responsive component to see if it starts working as expected (like in my codesandbox example). If it works, then you know the problem is somewhere in the children. If it does not work then there is a greater problem above in your app.
If you are using SSR, on initial load the content was rendered and served with the Responsive component having no knowledge of the viewport. So you may need to also add a CSS media query.
With current version of react-navigation, there are two ways to check if a screen is focused or not, by either (1) calling function isFocused of screen's prop navigation, or (2) hook the component to withNavigationFocused and retrieve prop isFocused.
However, both methods always return true when navigation starts. In my case, I need something triggering only when screen transition ends, i.e. new screen is fully focused. This is to deal with heavy-rendered children, such as camera or map, which should be rendered after screen transition to avoid slow transition animation.
Any idea how to achieve that?
You can try subscribing to the navigation lifecycle events, as described in the docs:
const didFocusSubscription = this.props.navigation.addListener(
'didFocus',
payload => {
console.debug('didFocus', payload);
}
);
Another example usage in this repo
I made a Todo list with React js. This web has List and Detail pages.
There is a list and 1 list has 10 items. When user scroll bottom, next page data will be loaded.
user click 40th item -> watch detail page (react-router) -> click back button
The main page scroll top of the page and get 1st page data again.
How to restore scroll position and datas without Ajax call?
When I used Vue js, i’ve used 'keep-alive' element.
Help me. Thank you :)
If you are working with react-router
Component can not be cached while going forward or back which lead to losing data and interaction while using Route
Component would be unmounted when Route was unmatched
After reading source code of Route we found that using children prop as a function could help to control rendering behavior.
Hiding instead of Removing would fix this issue.
I am already fixed it with my tools react-router-cache-route
Usage
Replace <Route> with <CacheRoute>
Replace <Switch> with <CacheSwitch>
If you want real <KeepAlive /> for React
I have my implementation react-activation
Online Demo
Usage
import KeepAlive, { AliveScope } from 'react-activation'
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Test />
</KeepAlive>
)}
</AliveScope>
)
}
The implementation principle is easy to say.
Because React will unload components that are in the intrinsic component hierarchy, we need to extract the components in <KeepAlive>, that is, their children props, and render them into a component that will not be unloaded.
Until now the awnser is no unfortunately. But there's a issue about it in React repository: https://github.com/facebook/react/issues/12039
keep-alive is really nice. Generally, if you want to preserve state, you look at using a Flux (Redux lib) design pattern to store your data in a global store. You can even add this to a single component use case and not use it anywhere else if you wish.
If you need to keep the component around you can look at hoisting the component up and adding a "display: none" style to the component there. This will preserve the Node and thus the component state along with it.
Worth noting also is the "key" field helps the React engine figure out what tree should be unmounted and what should be kept. If you have the same component and want to preserve its state across multiple usages, maintain the key value. Conversely, if you want to ensure an unmount, just change the key value.
While searching for the same, I found this library, which is said to be doing the same. Have not used though - https://www.npmjs.com/package/react-keep-alive
I have a fixed sidepanel that contains a search bar with filter buttons similar to https://facebook.github.io/react/docs/thinking-in-react.html
The list items are clickable and trigger history.push() for nested path urls.
The nested path structure is
/category-1
/category-1/product-1
/category-1/product-2
/category-2
/category-2/product-1
/category-2/product-2
.
.
.
/category-n/product-m
/category-n/product-k
User can scroll the sidepanel's list and select filters. However, a click on a list item (with onClick() followed by history.push()) causes the whole page (including the sidepanel) to render. This in turn doesn't keep the sidepanel's state so the filters and the scroll position are reset.
The parent render: function() looks like:
return(
<div id="main-view">
<Sidepanel history={this.props.history} properties={this.props.properties} />
React.cloneElement(this.props.children, {this.props});
</div>
);
The React.cloneElement() is the product info container that should render only.
I'm using react-router, react-redux and redux-simple-router.
Do I need to store the sidepanel's list scroll position and filter values outside of the sidepanel? Or is there a better way to keep the sidepanel's state between the url changes?
Your architecture is not aligned with what you want to do.
Simply render the navigation in your top-level, parent component and it will never lose state - for example in your Application component. Then SidePanel wouldn't be affected by route change.
Also you could pass down a callback to set / alter navigation from children, if needed (for example hide() or addMenuItems()).