import css still stays in page when route changes - javascript

I am using two main layouts with react router like below. I have written AppRoute component to use different layouts with switch.
Problem is when i come back to "/" home from "/login" or "/register" route some import css related to "LayoutLoginRegister" still stays in browser. So it breaks the page. Because this css belongs to "LayoutLoginRegister" not "LayoutLanding"
App.js
const AppWrapper = styled.div`
margin: 0 auto;
min-height: 100%;
`;
const AppRoute = ({ component: Component, layout: Layout, ...rest }) => (
<Route
{...rest}
render={(props) => (
<Layout>
<Component {...props} />
</Layout>
)}
/>
);
AppRoute.propTypes = {
component: React.PropTypes.any.isRequired,
layout: React.PropTypes.any.isRequired,
};
export default function App() {
return (
<AppWrapper>
<Helmet
titleTemplate="%s - React.js Boilerplate"
defaultTitle="React.js Boilerplate"
>
<meta name="description" content="A React.js Boilerplate application" />
</Helmet>
<Switch>
<AppRoute exact path="/" layout={LayoutLanding} component={HomePage} />
<AppRoute path="/features" layout={LayoutLanding} component={FeaturePage} />
<AppRoute path="/login" layout={LayoutLoginRegister} component={LoginPage} />
<AppRoute path="/register" layout={LayoutLoginRegister} component={RegisterPage} />
</Switch>
</AppWrapper>
);
}
LayoutLoginRegister Layout
import React from 'react';
import PropTypes from 'prop-types';
import './LayoutLoginRegister.scss';
export class LayoutLoginRegister extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
LayoutLoginRegister.propTypes = {
children: PropTypes.object.isRequired,
};
export default LayoutLoginRegister;

Importing CSS with webpack in this way will not load / unload the css when the component mounts / unmounts.
I would recommend namespacing your css styles, because you are using scss & separate files for your layouts, this should be fairly easy.
Wrap the contents of LayoutLoginRegister.scss with
.component-LayoutLoginRegister {
[layout login styles go here]
}
Then add a class to your LayoutLoginRegister component
render() {
return (
<div className="component-LayoutLoginRegister">
{this.props.children}
</div>
);
}
The CSS will remain loaded, but it will not affect anything other than your LayoutLoginRegister component.
If you need to apply styles to something shared, like <body>
You can add a class to the body / HTML element when a component mounts / unmounts.
export class MyComponent extends React.Component {
componentDidMount() {
document.body.classList.add('MyComponent-mounted');
}
componentDidUnmount() {
document.body.classList.remove('MyComponent-mounted');
}
}
Although I would generally avoid this unless absolutely necessary, as it couples your component with document.body and makes the component unpure, harder to reuse, etc.

Related

How can you wrap a story with a react-router layout route?

I have a very simple and plain ComponentX that renders some styled HTML, no data fetching or even routing needed. It has a single, simple story. ComponentX is meant to be used in a dark-themed website, so it assumes that it will inherit color: white; and other such styles. This is crucial to rendering ComponentX correctly. I won't bore you with the code for ComponentX.
Those contextual styles, such as background-color: black; and color: white;, are applied to the <body> by the GlobalStyles component. GlobalStyles uses the css-in-js library Emotion to apply styles to the document.
import { Global } from '#emotion/react';
export const GlobalStyles = () => (
<>
<Global styles={{ body: { backgroundColor: 'black' } }} />
<Outlet />
</>
);
As you can see, this component does not accept children, but rather is meant to be used as a layout route, so it renders an <Outlet />. I expect the application to render a Route tree like the below, using a layout route indicated by the (1)
<Router>
<Routes>
<Route element={<GlobalStyles/>} > <== (1)
<Route path="login">
<Route index element={<Login />} />
<Route path="multifactor" element={<Mfa />} />
</Route>
Not pictured: the <Login> and <Mfa> pages call ComponentX.
And this works!
The problem is with the Stories. If I render a plain story with ComponentX, it will be hard to see because it expects all of those styles on <body> to be present. The obvious solution is to create a decorator that wraps each story with this <Route element={<GlobalStyles/>} >. How can this be accomplished? Here's my working-but-not-as-intended component-x.stories.tsx:
import React from 'react';
import ComponentX from './ComponentX';
export default {
component: ComponentX,
title: 'Component X',
};
const Template = args => <ComponentX {...args} />;
export const Default = Template.bind({});
Default.args = {};
Default.decorators = [
(story) => <div style={{ padding: '3rem' }}>{story()}</div>
];
(I realize that I can make <GlobalStyles> a simple wrapper component around the entire <Router>, but I want to use this pattern to create stories for other components that assume other, intermediate layout routes.)
What I've usually done is to create custom decorator components to handle wrapping the stories that need specific "contexts" provided to them.
Example usage:
Create story decorator functions
import React from 'react';
import { Story } from '#storybook/react';
import { ThemeProvider } from '#mui/material/styles';
import CssBaseline from '#mui/material/CssBaseline';
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import theme from '../src/constants/theme';
import { AppLayout } from '../src/components/layout';
// Provides global theme and resets/normalizes browser CSS
export const ThemeDecorator = (Story: Story) => (
<ThemeProvider theme={theme}>
<CssBaseline />
<Story />
</ThemeProvider>
);
// Render a story into a routing context inside a UI layout
export const AppScreen = (Story: Story) => (
<Router>
<Routes>
<Route element={<AppLayout />}>
<Route path="/*" element={<Story />} />
</Route>
</Routes>
</Router>
);
.storybook/preview.js
import { INITIAL_VIEWPORTS } from '#storybook/addon-viewport';
import { ThemeDecorator } from './decorators';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
options: {
storySort: {
includeName: true,
method: 'alphabetical',
order: ['Example', 'Theme', 'Components', 'Pages', '*'],
},
},
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
},
},
};
export const decorators = [ThemeDecorator]; // <-- provide theme/CSS always
Any story that needs the app layout and routing context:
import React from 'react';
import { ComponentStory, ComponentMeta } from '#storybook/react';
import { AppScreen, MarginPageLayout } from '../../.storybook/decorators';
import BaseComponentX from './ComponentX';
export default {
title: 'Components/Component X',
component: BaseComponentX,
decorators: [AppScreen], // <-- apply additional decorators
parameters: {
layout: 'fullscreen',
},
} as ComponentMeta<typeof BaseComponentX>;
const BaseComponentXTemplate: ComponentStory<typeof BaseComponentX> = () => (
<BaseComponentX />
);
export const ComponentX = BaseComponentXTemplate.bind({});
In my example you could conceivably place all your providers and that Global component (w/ props) in what I've implemented as ThemeDecorator and set as a default decorator for all stories.

Multiple different layout alongside private route

I am trying to create a structure with multiple different layouts alongside private route to show the correct content based on user's log in status and assigned layout. Currently I have 3 different layouts but I may add another one in the future.
routes.js
import React from 'react';
import { LayoutOne, LayoutTwo, LayoutThree } from './layouts';
import RouteWithLayout from './components/RouteWithLayout/RouteWithLayout';
import Login from './components/Login/Login';
import Dash from './components/Dash/Dash';
import Home from './components/Home/Home';
import NotFound from './components/NotFound/NotFound';
import ErrorBoundary from './components/ErrorBoundary/ErrorBoundary';
const Routes = () => (
<ErrorBoundary>
<Switch>
<RouteWithLayout
component={Home}
exact
layout={LayoutOne}
path="/"
isPrivate={false}
/>
<RouteWithLayout
component={Dash}
exact
layout={LayoutTwo}
path="/dash"
isPrivate={true}
/>
<RouteWithLayout
component={Login}
exact
layout={LayoutThree}
path="/login"
isPrivate={false}
/>
<Route component={NotFound}/>
</Switch>
</ErrorBoundary>
);
export default Routes;
RouteWithLayout.js
import React from 'react';
import { Route } from 'react-router-dom';
import { authService } from "./services/auth.service";
const RouteWithLayout = props => {
const { layout: Layout, component: Component, private: isPrivate, ...rest } = props;
const isLogged = authService.isLogged();
return (
<Route
{...rest}
render={matchProps =>
isPrivate ? (
isLogged ? (
<Layout>
<Component {...matchProps} />
</Layout>
) : (
<Redirect
to={{
pathname: "/login",
state: { from: matchProps.location }
}}
/>
)
) : (
<Layout>
<Component {...matchProps} />
</Layout>
)
}
/>
)
};
export default RouteWithLayout;
please lmk I am doing this the correct way or I should take some other/better approach so simplify what I have been trying to achieve ?
You have multiple options to work with different Layouts.
The approach you have taken is good if you have multiple Routes that share a common Layout.
However if you have a lot of varying Layouts for different Routes, its better to actually render the Layout inside individual components directly like
const Dash = () => (
<LayoutOne>
{/* Dash component code */}
</LayoutOne>
)
You could even adopt the above approach with common Routes too as its easier to use and let the Route component do what its actually doing.
P.S. Frameworks like Gatsby actually handle multiple layouts by using them within each Pages, so this is a good pattern to follow

Best way to change React state at <App /> level based on sub-components

I have a React app that has a top-level <App /> component (built starting from create-react-app). My top level component looks something like this:
<Header title={this.state.appTitle} theme={this.state.theme} />
<Switch>
{routes.map((route, index) => <Route key={index} {...route} />)}
<Route component={NotFound} />
</Switch>
(routes is an array of { component:, to: } objects). Each component rendered by <Route> uses a sub-component called <Page> where I set a title and wrap some content:
<Page title="About Us">
<p>Content here</p>
</Page>
Sometimes a page might use a different theme which I want to apply to the <Header /> when that page is being viewed:
<Page title="About Us" theme="alt">
What I'm looking to do is change appTitle and theme state in <App /> when each component is rendered. What is the best way to do this? Using one of React's life-cycle hooks? Some other method to change the "top-level" state? How can I pass an action down to these components through the react-router <Route> component if so?
You can pass a function to each component, and call that function when each child gets mounted, with componentDidMount lifecycle method.
<Switch>
{routes.map((route, index) => {
const Comp = route.component;
return <Route
key={index}
{ ...route}
// overwrite the component prop with another component
component={
(routeProps) => (
<Comp
{...routeProps}
childHasMounted={() => this.setState({ name: route.name })}
/>
)
}
/>
})}
<Route component={NotFound} />
</Switch>
// child.js
class Child extends React.Component {
componentDidMount() {
this.props.childHasMounted();
}
}
Flip the structure on its head. Have every "page" component control their own layout.
Make a layout higher order component (function that takes a component class and returns a component class):
function LayoutHOC({ title, theme, component: ContentComponent }) {
return class LayoutWrapper extends React.Component {
render() {
return (
<div>
<Header title={title} theme={theme} />
<Page title={title} theme={them}>
<ContentComponent {...this.props} />
</Page>
</div>
)
}
}
}
Make the folder structure domain specific, as in pages/about/MyAboutPage.jsx to hold the main component content.
Then make pages/about/index.js and export the content component wrapped in the layout higher order component.
index.js:
import MyAboutPage from './MyAboutPage';
export default LayoutHOC({ title: 'my title', theme: 'alt', component: MyAboutPage })
Then in your routes you can import About from './pages/about' (since it uses index.js you don't have to worry about nested folder structure).
The downside is you have to create an index.js for each domain/route. The upside is your content component doesn't know about its layout, and every page/route can controll its own header/footer however you want.
This pattern is stolen from this React boilerplate project

Get path name of route globally in react

I have a basic routing SPA working using react-router-dom v4 and I would like to get the current route (path name) for use in my header (outer context of the app - not sure if that's accurate nomenclature). I want to have a button in my app bar which will do something depending on the route currently in use.
index.js
ReactDOM.render((
<MuiThemeProvider>
<div>
<Router history={history}>
<div>
<Header />
<MainView />
</div>
</Router>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
header.js
class Header extends React.Component {
constructor(props){
super(props);
this.state = {
open: false
};
}
toggleDrawer(){
this.setState({open: !this.state.open}, ()=> {console.log(this.state)});
}
render() {
return (
<div>
<AppBar
iconClassNameRight="muidocs-icon-navigation-expand-more"
onLeftIconButtonTouchTap={()=>{this.toggleDrawer()}}
iconElementRight={<FlatButton label="Create New"/>}
/>
...
In the header.js I want access to the route's pathname to call a certain function from the <FlatButton /> on the right of the appbar. I've tried {this.props.location.pathname} as per the v4 docs but only got errors. TBH I was probably using it wrong though.
That prop is only provided to components rendered as the child of a Route. If you want it somewhere else (like in your Header), you can use the withRouter helper method to inject the props into your component:
// Header.js
import { withRouter } from 'react-router';
// private header class
class Header extends React.Component {
render() {
// you can access this.props.location here
}
}
// wrap Header class in a new class that will render the
// Header class with the current location
// export this class so other classes will render this
// wrapped component
export default withRouter(Header);
// index.js
// ....
ReactDOM.render((
<MuiThemeProvider>
<div>
<Router history={history}>
<div>
<Header />
<MainView />
</div>
</Router>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
You should use react-router-dom if you are not using it (it's the default react-router package for the web now).
import { BrowserRouter, Route, Link } from 'react-router-dom';
ReactDOM.render((
<MuiThemeProvider>
<div>
<BrowserRouter>
<div>
<Route component={Header} />
<Route exact path="/" component={MainView} />
</div>
</BrowserRouter>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
and then from the header.js try using
this.props.location.pathname

Get properties from Routes

I'm failing at passing a property from a <Route />
Here is some code :
./app.jsx (main app)
import React from 'react'
import { render } from 'react-dom'
import { Router, Route, IndexRoute } from 'react-router'
import App from './components/app'
import Home from './components/home'
import About from './components/about'
render((
<Router>
<Route path="/" component={App}>
<IndexRoute component={Home} title="Home" />
<Route path="about" component={About} title="About" />
</Route>
</Router>
), document.getElementById('app'))
./components/app.jsx
import React from 'react';
import Header from './template/header'
class App extends React.Component {
render() {
return (
<div>
<Header title={this.props.title} />
{this.props.children}
</div>
)
}
}
export default App
./components/template/header.jsx
import React from 'react'
class Header extends React.Component {
render() {
return (
<span>{this.props.title}</span>
)
}
}
export default Header
When I click on my home route* I want my Header component to display Home.
When I click on my about route I want my Header component to display About.
At this point, my Header components displays nothing. this.props.title is undefined in my App component.
Looks like you can't pass an attribute from a <Route />
Is there a way to achieve this?
Or is there another way? For instance, can you get something from the children element (this.props.children.title or something like that) ?
It looks like the route injects a routes property with a list of the matching routes. The last route in the list has the props you specify. See http://codepen.io/anon/pen/obZzBa?editors=001
const routes = this.props.routes;
const lastRoute = routes[routes.length - 1];
const title = lastRoute.title;
I'd hesitate a little to use this, since routes is not documented in the Injected Props, so I don't know how reliable it is across version updates. A simpler option, though not as legible, would be to use this.props.location.pathname and maintain a lookup table for titles.
The best and most flexible option is probably the boilerplate-heavy one, where you define the template and then reuse it across components:
class Template extends React.Component {
render() {
return (
<div>
<Header title={this.props.title} />
{this.props.children}
</div>
)
}
}
class About extends React.Component {
render() {
return (
<Template title="About">
Some Content
</div>
)
}
}

Categories

Resources