How to detect if another component is present in the document? - javascript

I have a site built with React Static that has a Header component that is always present. Depending on if the current page has a hero component or not, the Header should be either light or dark.
The Header is rendered outside of the routes and the useEffect is triggered before the children is rendered. This is probably because of the routing.
This is the current code:
// App.js
import React, { useState, useEffect } from 'react'
import { Root, Routes } from 'react-static'
export default () => {
const [useDarkTheme, setUseDarkTheme] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
const heroPresent = document.querySelectorAll(".o-hero").length > 0;
console.log("The hero is present: " + heroPresent);
setUseDarkTheme(!heroPresent);
}
})
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<Header useDarkTheme={ useDarkTheme } />
<Routes default />
</React.Suspense>
</Root>
);
}
What will be rendered at <Routes default /> is the static pages configured in React Static's static.config.js.
Below is an example of the Hero component:
// Hero.js
import React from "react";
export default () => {
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
When I run the application and look at the logs this is what I get:
The hero is present: false
This is the Hero rendering. If this exist, the Header should be dark.
How could I somehow detect the presence of the Hero from the Header although the Hero is in a router and the Header is not? This feels like quite a common use case, but I could not find any info on the interwebs.
Thanks in advance!

So I ended up using useContext to provide all children with a getter and a setter for the Header's theme (dark or light). The solution is very much inspired from this answer. The solution looks like this:
// App.js
import React, { useState, useContext } from 'react'
import { Root, Routes } from 'react-static'
import { HeaderThemeContext } from "./context";
export default () => {
const { theme } = useContext(HeaderThemeContext);
const [headerTheme, setHeaderTheme] = useState(theme);
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<HeaderThemeContext.Provider value={ { theme: headerTheme, setTheme: setHeaderTheme } }>
<Header theme={ headerTheme } />
<Routes default />
</HeaderThemeContext.Provider>
</React.Suspense>
</Root>
);
}
// Hero.js
import React from "react";
import { headerThemes, setHeaderTheme } from "./context";
export default () => {
setHeaderTheme(headerThemes.DARK);
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
// context.js
import React, { createContext, useContext } from "react";
export const headerThemes = {
LIGHT: "light",
DARK: "dark",
};
export const HeaderThemeContext = createContext({
theme: headerThemes.LIGHT,
setTheme: () => {}
});
// This is a hook and can only be used in a functional component with access to the HeaderThemeContext.
export const setHeaderTheme = theme => useContext(HeaderThemeContext).setTheme(theme);
This gives global access to set and get the header theme, which might not be optional, but it works for now and I think it's fine. Please let me know if there is a better way of doing this.

Related

Next JS Layout component, pass props to children

I have found a code that solved my problem in Next JS re rendering when changing pages. But now i need to send props to the children component. I got no idea how i can make it works here, this is my layout.js code. As you can see i can send props to Header component but for children i dont know how, because it is a variable and not a component.
import Header from "../components/header";
import Footer from "../components/footer";
import { Fragment } from "react";
export default function Layout({ children, ...pageProps }) {
return (
<Fragment>
<Header
isRegisterPage={pageProps.isRegisterPage}
isLoginPage={pageProps.isLoginPage}
outHome={pageProps.outHome}
/>
{children}
<Footer />
</Fragment>
);
}
Thank you for the help
Have you considered using React's Context API? The idea is that when using the Context API your component's state get's lifted, to be managed at a global scale. If a component needs a prop, instead of passing props down manually (prop drilling) you can simply wrap you component in what's known as a context provider. This will allow that Component to access the global state of your application. This is good because, when your application gets bigger, you may need to pass props down through many components which can clutter and add unneeded confusion.
React provides some great documentation to set your React application up to use the Context API. Highly recommend checking it out!
https://reactjs.org/docs/context.html
Try this
import Header from "../components/header";
import Footer from "../components/footer";
import { Fragment } from "react";
export default function Layout({ children, ...pageProps }) {
function recursiveMap(children, fn) {
return React.Children.map(children, child => {
if (!React.isValidElement(child) || typeof child.type == 'string') {
return child;
}
if (child.props.children) {
child = React.cloneElement(child, {
children: recursiveMap(child.props.children, fn)
});
}
return fn(child);
});
}
// Add props to all child elements.
const childrenWithProps = recursiveMap(children, child => {
// Checking isValidElement is the safe way and avoids a TS error too.
if (isValidElement(child)) {
// Pass additional props here
return cloneElement(child, { currentUser: { ...user } })
}
return child;
});
return (
<Fragment>
<Header
isRegisterPage={pageProps.isRegisterPage}
isLoginPage={pageProps.isLoginPage}
outHome={pageProps.outHome}
/>
{childrenWithProps}
<Footer />
</Fragment>
);
}
You can use React's cloneElement to achieve that.
React.cloneElement(children, {
isRegisterPage: pageProps.isRegisterPage,
isLoginPage: pageProps.isLoginPage,
outHome: pageProps.outHome
})
Complete example in your case:
import Header from "../components/header";
import Footer from "../components/footer";
import React, { Fragment } from "react";
export default function Layout({ children, ...pageProps }) {
return (
<Fragment>
<Header
isRegisterPage={pageProps.isRegisterPage}
isLoginPage={pageProps.isLoginPage}
outHome={pageProps.outHome}
/>
{
React.cloneElement(children, {
isRegisterPage: pageProps.isRegisterPage,
isLoginPage: pageProps.isLoginPage,
outHome: pageProps.outHome
})
}
<Footer />
</Fragment>
);
}
From the answer of Lucas Raza, below is an example that uses Context API to apply themes to different components
1.Create a context File
//ThemeContex.js
import { createContext, useState } from "react";
export const ThemeContext = createContext();
export const withThemeContext = Component => {
const WrappedComp = props => {
const [darkColor,lightColor] = ["#3b3b3b", "#ddd"]
const [lightBackgoround,darkBackgoround] = ["#ececec","#1d2a35"]
const darkTheme = {
backgroundColor: darkBackgoround,
color:lightColor,
}
const lightTheme = {
backgroundColor: lightBackgoround,
color:darkColor,
}
const themes = {
darkTheme, lightTheme
}
const [theme, setTheme] = useState(lightTheme)
const children ={
theme,
themes,
setTheme,
}
return(
<StylesContext.Provider value={{...children}} >
<Component {...props} />
</StylesContext.Provider>
)
}
return WrappedComp;
}
In _app.js, import withThemeContext higher component and wrap MyApp with it when exporting it.
import { withThemeContext } from '../components'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default withThemeContext(MyApp)
You can know use theme any where in a component
import { useContext } from 'react'
import {ThemeContext} from '../components'
export default function Home() {
const { theme } =useContext(ThemeContext)
return (
<div id="home" style={theme}>
// Home logic...
</div>
)
}

Passing react-router-dom's Link into external library

I'm rendering components from my external (node_modules) pattern library. In my main App, I'm passing my Link instance from react-router-dom into my external libraries' component like so:
import { Link } from 'react-router-dom';
import { Heading } from 'my-external-library';
const articleWithLinkProps = {
url: `/article/${article.slug}`,
routerLink: Link,
};
<Heading withLinkProps={articleWithLinkProps} />
In my library, it's rendering the Link as so:
const RouterLink = withLinkProps.routerLink;
<RouterLink
to={withLinkProps.url}
>
{props.children}
</RouterLink>
The RouterLink seems to render correctly, and even navigates to the URL when clicked.
My issue is that the RouterLink seems to have detached from my App's react-router-dom instance. When I click Heading, it "hard" navigates, posting-back the page rather than routing there seamlessly as Link normally would.
I'm not sure what to try at this point to allow it to navigate seamlessly. Any help or advice would be appreciated, thank you in advance.
Edit: Showing how my Router is set up.
import React from 'react';
import { hydrate, unmountComponentAtNode } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'react-router-redux';
import RedBox from 'redbox-react';
import { Route } from 'react-router-dom';
import { Frontload } from 'react-frontload';
import App from './containers/App';
import configureStore from './redux/store';
import withTracker from './withTracker';
// Get initial state from server-side rendering
const initialState = window.__INITIAL_STATE__;
const history = createBrowserHistory();
const store = configureStore(history, initialState);
const mountNode = document.getElementById('react-view');
const noServerRender = window.__noServerRender__;
if (process.env.NODE_ENV !== 'production') {
console.log(`[react-frontload] server rendering configured ${noServerRender ? 'off' : 'on'}`);
}
const renderApp = () =>
hydrate(
<AppContainer errorReporter={({ error }) => <RedBox error={error} />}>
<Provider store={store}>
<Frontload noServerRender={window.__noServerRender__}>
<ConnectedRouter onUpdate={() => window.scrollTo(0, 0)} history={history}>
<Route
component={withTracker(() => (
<App noServerRender={noServerRender} />
))}
/>
</ConnectedRouter>
</Frontload>
</Provider>
</AppContainer>,
mountNode,
);
// Enable hot reload by react-hot-loader
if (module.hot) {
const reRenderApp = () => {
try {
renderApp();
} catch (error) {
hydrate(<RedBox error={error} />, mountNode);
}
};
module.hot.accept('./containers/App', () => {
setImmediate(() => {
// Preventing the hot reloading error from react-router
unmountComponentAtNode(mountNode);
reRenderApp();
});
});
}
renderApp();
I've reconstructed your use case in codesandbox.io and the "transition" works fine. So maybe checking out my implementation might help you. However, I replaced the library import by a file import, so I don't know if that's the decisive factor of why it doesn't work without a whole page reload.
By the way, what do you mean exactly by "seamlessly"? Are there elements that stay on every page and should not be reloaded again when clicking on the link? This is like I implemented it in the sandbox where a static picture stays at the top on every page.
Check out the sandbox.
This is the example.js file
// This sandbox is realted to this post https://stackoverflow.com/q/59630138/965548
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Heading } from "./my-external-library.js";
export default function App() {
return (
<div>
<img
alt="flower from shutterstock"
src="https://image.shutterstock.com/image-photo/pink-flowers-blossom-on-blue-600w-1439541782.jpg"
/>
<Router>
<Route exact={true} path="/" render={Welcome} />
<Route path="/article/coolArticle" component={CoolArticleComponent} />
</Router>
</div>
);
}
const Welcome = () => {
const articleWithLinkProps = {
url: `/article/coolArticle`,
routerLink: Link
};
return (
<div>
<h1>This is a super fancy homepage ;)</h1>
<Heading withLinkProps={articleWithLinkProps} />
</div>
);
};
const CoolArticleComponent = () => (
<div>
<p>This is a handcrafted article component.</p>
<Link to="/">Back</Link>
</div>
);
And this is the my-external-library.js file:
import React from "react";
export const Heading = ({ withLinkProps }) => {
const RouterLink = withLinkProps.routerLink;
return <RouterLink to={withLinkProps.url}>Superlink</RouterLink>;
};

Does returning null in a component prevents child components from rendering?

I have a LoadingProvider where I set the state of my Loading component to false and when needed to true. I want to show my Loading component only when the state of loading equals to true.
All my providers, router and app components are loaded in my root.js:
import React, { Component } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import { MuiPickersUtilsProvider } from "material-ui-pickers";
import MomentUtils from "#date-io/moment";
import App from "./App";
import { DeleteDialogProvider } from "/hocs/withDeleteDialog";
import { WarningDialogProvider } from "/hocs/withWarningDialog";
import { LoadingProvider } from "/hocs/withLoading";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import { StateProvider } from "/hocs/withState";
import { I18nProvider } from "/hocs/withI18n";
const theme = createMuiTheme({});
class Root extends Component {
render() {
return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<I18nProvider>
<DeleteDialogProvider>
<WarningDialogProvider>
<StateProvider>
<Router>
<LoadingProvider>
<App />
</LoadingProvider>
</Router>
</StateProvider>
</WarningDialogProvider>
</DeleteDialogProvider>
</I18nProvider>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
);
}
}
export default Root;
My other providers don't block any other components from rendering. But when I add the LoadingProvider in root.js and check the console with the React Developer Tools I see it doesn't load/render the components that comes after my LoadingProvider component. The problem is that I don't know why it doesn't render any other components.
This is my withLoading file where I define the LoadingProvider:
import React, { Component } from "react";
import Loading from "/components/Loading";
const LoadingContext = React.createContext();
export class LoadingProvider extends Component {
constructor(props) {
super(props);
this.state = {
loading: false
};
}
setLoadingContext = e => {
this.setState({
loading: true
});
};
render() {
return (
<LoadingContext.Provider value={this.setLoadingContext}>
<Loading
loading={this.state.loading}
/>
</LoadingContext.Provider>
);
}
}
export const withLoading = Component => props => (
<LoadingContext.Consumer>
{setLoadingContext => (
<Component {...props} setLoadingContext={setLoadingContext} />
)}
</LoadingContext.Consumer>
)
And this is my Loading.js file where I define my Loading component:
import React, { Component } from 'react';
import CircularProgress from '#material-ui/core/CircularProgress';
class Loading extends Component {
render() {
const loading = this.props;
// TODO: fix weird repetitive loading prop
if (!loading.loading) {
return null;
} else {
return (
<CircularProgress />
);
}
}
}
export default Loading;
I guess it has something to do with returning null when loading is false. But when I comment that rule of code out it says:
Uncaught Invariant Violation: Loading(...): Nothing was returned from
render. This usually means a return statement is missing. Or, to
render nothing, return null.
This is primarily because in your LoadingProvider you are not using props.children.
<LoadingContext.Provider value={this.setLoadingContext}>
<Loading
loading={this.state.loading}
/>
{this.props.children} // add this
</LoadingContext.Provider>
Take note that null don't render anything.
Your <App/> is passed to LoadingProvider in its children property. But LoadingProvider doesn't do anything with its children, so nothing happens.
So return this.props.children when you want them to render.

this.props.match.params passed into child component after authorisation

I have recently started building a big project on React using also a Firebase with authentication and I cannot quite understand the relation between the react-router-dom links and React components.
I am struggling with getting the
this.props.match.params // which is going to be 2018 / 2019 / 2020... etc
in the component, which renders as a dynamic route (like unique post component).
I have tried to use only a simple class component and this works but the problem is, without the authentication everyone can access this admin route and everyone would be allowed to edit and delete data there. I want it to be accessed only by authenticated users. (Admins)
So this is how my piece of code looks like:
Main component: (where the link is)
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
class SeasonBox extends Component {
render() {
return (
<Link className='seasonbox' to={`/adminseason/${this.props.season}`}>
<p className='seasonbox__season'>{this.props.season}/{this.props.season+1}</p>
</Link>
)
}
}
export default SeasonBox;
And the component that renders after the link is clicked:
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withAuthorisation } from '../Session'
import { withFirebase } from '../Firebase'
const AdminMatchesBox = ({authUser}) => (
<div>{authUser ? <AdminMatchesBoxAuth /> : <AdminMatchesBoxNonAuth />} </div>
)
class AdminMatchesBoxAuth extends Component {
render() {
return (
<div>
Hey I am the season {this.props.match.params}!
<Link to={'/adminmatches'}>Wróć</Link>
</div>
)
}
}
const AdminMatchesBoxNonAuth = () => (
<div>
<h1>You do not have permission to visit this page.</h1>
</div>
)
const mapStateToProps = state => ({
authUser: state.sessionState.authUser
});
const condition = authUser => !!authUser
export default compose(withAuthorisation(condition), connect(mapStateToProps),withFirebase)(AdminMatchesBox);
So if I don't use authorisation, and I use only a single class component I can get this.props.match.params -> which is the id of the website and I need it to access data from the database.
However, I want it to not be visible by not logged users and I had to process it through the authorisation process.
I am receiving an error
Cannot read property 'params' of undefined.
I have no clue how to pass match.params into the AdminMatchesBoxAuth component.
Could anyone advice?
By wrapping withRouter you able to access params
Try this
import { withRouter } from "react-router";
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withAuthorisation } from '../Session'
import { withFirebase } from '../Firebase'
const AdminMatchesBox = ({authUser}) => (
<div>{authUser ? <AdminMatchesBoxAuth /> : <AdminMatchesBoxNonAuth />} </div>
)
class AdminMatchesBoxAuth extends Component {
constructor (props){
super(props)
}
render() {
return (
<div>
Hey I am the season {this.props.match.params}!
<Link to={'/adminmatches'}>Wróć</Link>
</div>
)
}
}
const AdminMatchesBoxNonAuth = () => (
<div>
<h1>You do not have permission to visit this page.</h1>
</div>
)
const mapStateToProps = state => ({
authUser: state.sessionState.authUser
});
const condition = authUser => !!authUser
export default compose(withRouter, withAuthorisation(condition), connect(mapStateToProps),withFirebase)(AdminMatchesBox)

Can't access url params, passed using react-router 4

I'm having issues accessing a parameter called bookId from the Reader.js component. The parameter is passed down from BookCarouselItem.js using react-router. Reader.js is a connected component.
I'm not sure if that makes a difference, but does react-router work with redux connected components? Or do I need to use something like connected-react-router?
I've tried to refer to similar questions but wasn't able to find a solution, help would be greatly appreciated.
App.js
import React, { Component } from 'react'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import { routes } from 'constants/index';
import Reader from 'components/reader/Reader'
Class App extends Component {
render() {
return (
<div className='container-fluid main-container'>
<Router>
<div>
<Route
path={'/reader/:bookId'}
component={() => <Reader />}
/>
</div>
</Router>
</div>
);
}
}
export default App;
BookCarouselItem.js
import React from 'react'
import { Link } from 'react-router-dom'
export class BookCarouselItem extends React.Component {
render() {
const { bookThumbnail } = this.props;
const { name, numberOfSections } = bookThumbnail;
const bookId = 0;
return (
<Link className='book-carousel-link' to={`/reader/${bookId}`}>
<div className='book-info-overlay'>
<h5>{name}</h5>
<span>{numberOfSections} Sections</span>
</div>
</Link>
);
}
}
export default BookCarouselItem;
Reader.js
import React from 'react'
import { connect } from 'react-redux'
import { compose } from 'recompose'
export class Reader extends React.Component {
render() {
const { match, pageLevel } = this.props;
console.log(match); // undefined
return (
<div>
<div className='reader-body'>
<Book bookId={match.params.bookId}
pageLevel={pageLevel}
bank={bank}/>
</div>
);
}
}
Const mapStateToProps = (state) => {
return {
metadata: state.book.metadata,
pageLevel: state.book.pageLevel
}
};
const authCondition = (authUser) => !!authUser;
export default compose(
withAuthorization(authCondition),
connect(mapStateToProps, mapDispatchToProps),
)(Reader);
You can just give the component to the component prop and the route props will be passed down to the component automatically.
<Route
path="/reader/:bookId"
component={Reader}
/>
If you want to render something that is not just a component, you have to pass down the route props manually.
<Route
path="/reader/:bookId"
render={props => <Reader {...props} />}
/>
I'm not sure but maybe mapStateToProps rewrite you props so could you please first read this issue

Categories