How to make a lazy loading high order component in React - reactjs

So here is the main idea, HOC that is be able to load any wrapped in component with React.lazy and React.Suspense. Is it possible???
So, I already was able to write some, but not sure that I was able to made properly...
import React, { Suspense, lazy, useState, useEffect } from "react"
export function withLazyImport(LazyComponent) {
return (props) => {
const [loadedComponent, setLoadedComponent] = useState(null)
useEffect(() => {
setLoadedComponent(lazy(() => import(<LazyComponent {...props} />)))
//eslint-disable-next-line
}, [])
return (
<Suspense fallback="Lazy component is loading ...">
{loadedComponent}
</Suspense>
)
}
}

I don't understand why do you use useEffect. The resulted component will not pass new props to the lazy component because props are passed on did mount.
I came up with another solution based on the example provided by the author of this question
import React, { Suspense } from 'react';
export const withLazyComponent = (LazyComponent) => {
return (props) => (
<Suspense fallback="Lazy component is loading ...">
<LazyComponent {...props} />
</Suspense>
)
}
And then you use it like:
const LazyComponent = withLazyComponent(React.lazy(() => import('path/to/component')));

Here's how to achieve it in TypeScript
import { Loader } from "../components/loader";
import { Suspense } from "react";
/**
* HOC to wrap a component in a Suspense component.
*/
export default function withSuspense<P>(Component: React.ComponentType & any) {
return function WithSuspense(props: P) {
return (
<Suspense fallback={<Loader />}>
<Component {...props} />
</Suspense>
);
};
}

you can try using already existing solutions like Loadable Components

Related

Tutorial uses a React Hook, but I'm working in a Class Component setting

I'm trying to follow this tutorial to implement Google Analytics with pageview tracking in my React application. However, the tutorial uses a React hook, while I've set up my application using class components. I've unsuccesful to translate the tuturial to a class setting. How should I adjust to make this work for my use case?
Routes page, \src\pages\index.js:
// the function of concern
import useGaTracker from "../useGaTracker";
class Base extends Component {
render() {
useGaTracker();
// The hook inside a Class component, which is not allowed.
// But how can I make it work in my class components setting?
function withProps(Component, props) {
return function (matchProps) {
return <Component {...props} {...matchProps} />;
};
}
return (
<Router history={history}>
<Switch>
<Route exact path="/" component={Homepage} />
// Etc.
GA function, \src\useGaTracker.js:
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import ReactGA from "react-ga";
const useGaTracker = () => {
const location = useLocation();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
ReactGA.initialize(process.env.REACT_APP_GA_TAG);
setInitialized(true);
}, []);
useEffect(() => {
if (initialized) {
ReactGA.pageview(window.location.pathname + window.location.search);
}
}, [initialized, location]);
};
export default useGaTracker;
And \src\index.js:
import Base from "./pages";
render(
<Provider store={store}>
<BrowserRouter>
<Base />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
Calling upon the GA function inside he Base class produces the error:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component.
How should I rewrite this to make it work in my Class components setting?
You could make a higher-order component create a functional component, and have that one call useGaTracker before rendering the given non-functional Component:
const withGaTracker = (Component) => {
return (props) => {
useGaTracker();
return (
<Component {...props} />
);
};
};
Then pass in the Base component
const BaseWithGaTracker = withGaTracker(Base);
and render the result in your index.js by replacing <Base /> with <BaseWithGaTracker />.
edit: Simpler yet, just make a single functional component that calls the hook and renders its children:
const GaTracker = ({children}) => {
useGaTracker();
return children;
}
then wrap that around <Base /> in index.js
<GaTracker>
<Base />
</GaTracker>
If you're not gonna use that hook anywhere else, you could also just inline it in the new component.

React Lazy Load useEffect

I have a problem getting clientWidth from useEffect.
There is a lazyLoad which loads a page with users.
import {Suspense, lazy} from 'react';
const UsersContainer = lazy (() => import ('./ users / users-container'));
const Index = (props) => {
return (
<Suspense fallback = {<div id = {'loading'} />}>
<UsersContainer />
</Suspense>
)
}
export default Index;
UsersContainer has a child component dataTooltip, which displays the full User name if it does not fit into the block.
import React, {useEffect, useRef} from 'react';
import '../common.scss';
const DataTooltip = ({title, ... props}) => {
let ref = useRef ();
useEffect (() => {
if (ref.current.scrollWidth> ref.current.clientWidth) {
ref.current.setAttribute ('data-tooltip', title)
}
});
return (
<div ref = {ref} className = {'tooltip'}>
{
React.cloneElement (props.children, {ref: ref})
}
</div>
)
}
export default DataTooltip;
What's the problem?
After the UsersContainer is loaded and rendered in the DOM, it has 'display: none' and at the same moment useEffect in DataTooltip is triggered asynchronously.
As a result, DataTooltip says that clientWidth = 0, due to the fact that the parent has 'display: none'.
How to make useEffect work after lazyLoad removed 'display: none'.
PS: useLayoutEffect works the same, clientWidth = 0
Solved the problem this way:
<Suspense fallback={<div id={'loading'}/>}>
<Main/>
<Acquaintance/>
<UsersContainer/>
<RegisterContainer/>
</Suspense>
to
<Suspense fallback={<div id={'loading'}/>}>
<Main/>
</Suspense>
<Suspense fallback={<div id={'loading'}/>}>
<Acquaintance/>
</Suspense>
<Suspense fallback={<div id={'loading'}/>}>
<UsersContainer/>
</Suspense>
<Suspense fallback={<div id={'loading'}/>}>
<RegisterContainer/>
</Suspense>
I don't know if this solves your issue - but one thing I notice immediately is that you're missing the dependency array from your useEffect hook.
import React, {useEffect, useRef} from 'react';
...
const DataTooltip = ({title, ... props}) => {
let ref = useRef ();
useEffect (() => {
if (ref.current.scrollWidth> ref.current.clientWidth) {
ref.current.setAttribute ('data-tooltip', title)
}
});
return (...)
}
export default DataTooltip;
should be:
import React, {useEffect, useRef} from 'react';
...
const DataTooltip = ({title, ... props}) => {
let ref = useRef ();
useEffect (() => {
if (ref.current.scrollWidth> ref.current.clientWidth) {
ref.current.setAttribute ('data-tooltip', title)
}
}, [ref]);
return (...)
}
export default DataTooltip;
Also keep in mind that this will cause the component to re-render whenever ref changes, per the documentation of the useEffect hook you should always declare any variables from the upper scope used in the useEffect hook as part of the dependency array, and if you dont dont use any such variables you should pass an empty array still to prevent running an infinite loop.

How to provide context from contextApi for a specific set of routes in nextjs and preserve state with routing through linking?

I am using contextApi with nextjs and I'm having some trouble when providing a context just for certain routes. I am able to make the context available for just a few routes, but when I transition from one to the other through linking, I end up losing the state of my application.
I have three files inside my pages folder:
index.tsx,
Dashboard/index.tsx and
SignIn/index.tsx.
If I import the provider inside the files Dashboard/index.tsx and SignIn/index.tsx and go from one page to the other by pressing a Link component from next/link, the whole state is set back to the initial state.
The content of the Dashboard/index.tsx file
import React from 'react';
import Dashboard from '../../app/views/Dashboard';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
export default Index;
This is the contend of the SignIn/index.tsx file:
import React from 'react';
import SignIn from '../../app/views/SignIn';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<SignIn />
</AuthProvider>
);
export default Index;
The views folder is where I create the components that will be rendered.
The content of the file views/SignIn/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const SignIn: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="Dashboard">Go back to Dashboard</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default SignIn;
And the content of the file views/Dashboard/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const Dashboard: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="SignIn">Go back to sign in page</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default Dashboard;
I am able to access the context inside both /Dashboard and /SignIn, but when I press the link, the state comes back to the initial one. I figured out that the whole provider is rerenderized and therefore the new state becomes the initial state, but I wasn't able to go around this issue in a "best practices manner".
If I put the provider inside _app.tsx, I can maintain the state when transitioning between pages, but I end up providing this state to the / route as well, which I am trying to avoid.
I was able to go around this by doing the following, but it really does not seem to be the best solution for me.
I removed the Providers from Pages/SignIn/index.tsx and Pages/Dashboard/index.tsx and used the following snippet for the _app.tsx file:
import React from 'react';
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { AuthProvider } from '../contexts/auth';
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const router = useRouter();
const AuthProviderRoutes = ['/SignIn', '/Dashboard'];
return (
<>
{AuthProviderRoutes.includes(router.pathname) ? (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
) : <Component {...pageProps} />}
</>
);
};
export default App;
Does anyone have a better solution?

React router 'to' through props in const

I suspect this is more about React (or JS) than routing.
To avoid props collision, I am using forwardRef, defined in a const. The 'to' I want to set through props passed from parent component, and I cannot get this work. In the example below, where it says to={props.toUrl} it only works if it is to='/hello'. How do I do this, so I can use the ButtonRouter component from the Demo component?
import React from "react";
import { MemoryRouter as Router } from "react-router";
import { Link } from "react-router-dom";
import Button from "#material-ui/core/Button";
const CollisionLink = React.forwardRef((props, ref) => (
<Link innerRef={ref} to={props.toUrl} {...props} />
));
function ButtonRouter(props) {
return (
<Router>
<Button component={CollisionLink}>Routing w/o props collision</Button>
</Router>
);
}
export default function Demo() {
return <ButtonRouter toUrl="/hello" />;
}
I think you might be missing props from ButtonRouter:
function ButtonRouter(props) {
return (
<Router>
<Button component={(routerProps) => <CollisionLink {...routerProps} {...props} />}>Routing w/o props collision</Button>
</Router>
);
}

React Hooks Mobx: invalid hook call. Hooks can only be called inside of the body of a function component

I am using React Hooks and when I wrap my component with observer from mobx, I am getting this error. What can be the problem? Is it possible to use mobx with React Hooks?
import classnames from 'classnames';
import { observer } from 'mobx-react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import ViewStore from '../../../store/ViewStore';
interface INavbarProps {
viewStore: ViewStore;
}
const Navbar = observer((props: INavbarProps) => {
const { authed } = props.viewStore;
const [drawerIsOpen, setState] = useState(false);
function toggleMenu() {
drawerIsOpen ? setState(false) : setState(true);
}
return (
<div>
<Link to="/">Home</Link>
<Link to="/admin">Admin</Link>
<Link to="/all">All</Link>
{authed ? <Link to="/">Logout</Link> : <Link to="/login">Login</Link>}
<div onClick={toggleMenu}>
Open Menu
</div>
<div className={classnames('drawer', {
drawer_open: drawerIsOpen,
})} />
<div onClick={toggleMenu} className={drawerIsOpen ? 'backdrop' : ''}></div>
</div>
);
});
export default Navbar;
It's currently not supported to use hooks with the mobx-react package since it creates a class component under the hood.
You can use the 6.0.0 release candidate that will be released soon, which does support hooks.
package.json
{
"dependencies": {
"mobx-react": "6.0.0-rc.4"
}
}
The MobX observer will result in a class component under the hood.
As per here, you can work around this by doing:
(props) => <Observer>{() => rendering}</Observer>
instead of:
observer((props) => rendering)
Example
import React from "react";
import { Observer } from "mobx-react";
const MobxFunctionalComponentObserver = props => (
<Observer
inject={stores => ({ ... })}
render={props => (
...
)}
/>
);

Resources