Cannot set a Loader while big component renders - reactjs

I want to show a loader while my Analysis component (and its children) load but I cannot for the life of me get this to work.
The main idea is:
<Context>
<Admin>
<Analysis (with other child components) />
</Admin>
</Context>
The Admin component has a sidebar and the main view that is based upon a Switch/Router as seen here:
const Admin = () => {
const classes = useStyles();
const { pageLoading } = useContext();
return (
<>
<Box display="flex">
<Sidebar
routes={sidebarRoutes}
logo={{
innerLink: "/",
imgAlt: "...",
}}
/>
<Box position="relative" flex="1" className={classes.mainContent}>
<Switch>
{ pageLoading ?
<Loader />
: (<Route
path="/admin/stats"
component={Analysis}
key={"stats"} />) }
</Switch>
<Container
maxWidth={false}
component={Box}
classes={{ root: classes.containerRoot }}
>
</Container>
</Box>
</Box>
</>
);
};
However, since the Analysis component and its children take a while to load, I want to display a loader like this (no API calls are made):
My current loading screen
See, the problem is that I tried setting the context loading state in my useEffect hook inside the Sidebar component, like this:
const handleTabClick = () => {
setPageLoading(true);
};
And then stopping the loader using the same context after the Analysis component mounts:
React.useEffect(() => {
setPageLoading(false);
}, []);
...And the loader gets stuck forever...
My conclusion is that when the Context component has its state changed and re-renders, the Admin component then has pageLoading field set to "true" and doesn't display the Analysis component.
What can I do to fix this? It's not crucial for my website but I'm trying to find a solution to this clunkiness. Clicking on the "Analysis" tab changes URLs in my browser search bar above, but nothing happens for a few seconds while the Analysis component loads. Looks stupid.

I managed to solve this with following ugly solution:
import React, { useEffect, useState } from "react";
import { Route } from "react-router-dom";
import Loader from "./Loader";
function SubPageLoader({path, component, key}) {
const [updates, setUpdates] = useState(0);
useEffect(() => {
// stupid solution to postpone loading of children, so we can show a loading screen. TODO: find a better method
const interval = setInterval(() => {
setUpdates(updates => updates + 1);
}, 1);
if (updates > 1) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [updates]);
return (
<>
{updates == 0 ? <Loader />
: <Route
path={path}
component={component}
key={key}
/> }
</>
);
}
export default SubPageLoader;
This basically postpones the children load by 1 millisecond and that way we can instantly enter the new tab and show a loader without having to update parent states.

Related

How to route between pages with minimum re-render in a dashboard in Next.js

The Dashboard looks something like this:
export default function Dashboard({ children }) {
return (
<>
<DashboardLayout menu={menu}>
<DashboardPage variant="cards" flow="one-one">
{children}
</DashboardPage>
</DashboardLayout>
</>
)
}
The Dashboard has a sidebar menu on the left which allows for navigating between different DashboardPages. Because the Dashboard pages all share components like the menu, the sidebar, the footer, etc., I ideally don't want to re-render these components.
If I use the Next.JS native <Link> component, then the all components get re-rendered.
The only alternative I see to this is using a React hook like useState or useReducer to set which pages gets rendered as such:
export default function Dashboard() {
const [state, dispatch] = useReducer();
return (
<>
<DashboardLayout menu={menu}>
<DashboardPage variant="cards" flow="one-one">
{state == 'page1' && <DashboardPage1 />}
{state == 'page2' && <DashboardPage1 />}
{state == 'page3' && <DashboardPage1 />}
...
{state == 'pageN' && <DashboardPageN />}
</DashboardPage>
</DashboardLayout>
</>
)
}
Is there a way to use routing such as or the useRoute hook and avoid re-rendering certain components? For example, whenever I change between dashboard pages, I see that a console log inside the "DashboardLayout", which does not need to re-render, gets printed 4 times.
You can use the built-in tag of NextJS and NextJS Router
{/* Adding the Link Component */}
<Link href="/"> Dashboard</Link>
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
);
}
Or you can use React-Router-DOM for navigation using useNavigate hook
An eg. of React-router
import { useNavigate } from "react-router-dom";
const Page = () => {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/pagename')}>
Go To Page
</button>
);
}

rendering a component inside a react page using Menu

I have a menu and the main body of the page. I have created different pages as components with some text. All I want is that when I click on the side bar menu, the components are displayed in the main page. How can I make this work?
const items2 = [{
label: 'Rice',
key: 'rice',
},
{
label: 'AB Test',
key: 'ab',
}]
const MainLayout = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
return (
<Layout>
<Sider >
<Menu
mode="inline"
items={items2}
onClick = {onClick}
/>
</Sider>
<Content >
//I Want to open the pages here
</Content>
</Layout>
</Content>
To render a component inside other component, React provides a special props name children.
To achieve your requirement, you can do like this:
MainLayout.js:
export const MainLayout = ({children}) => {
const {
token: { colorBgContainer },
} = theme.useToken();
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
return (
<Layout>
<Sider>
<Menu
mode="inline"
items={items2}
onClick={onClick}
/>
</Sider>
<Content>
{children}
</Content>
</Layout>
)
}
In MainLayout.js, you only need to write {children} inside component <Content>, React will do the rest for you to pass the content of Rice or AB or whatever for you. In each page, just place <MainLayout> at the top of the outer of rest of your code.
Please see 2 example files below.
Rice.js:
import MainLayout from './MainLayout';
export const Rice = () => {
// Do some stuffs here...
return (
<MainLayout>
<div>
<h2>Best rated rice in the World</h2>
<ol>
<li>Pinipig</li>
<li>Riz de Camargue</li>
...
</ol>
<div>
</MainLayout>
)
}
Corn.js:
import MainLayout from './MainLayout';
export const Corn = () => {
// Do some stuffs here...
return (
<MainLayout>
<div>
<h2>Best Corn Side Dish Recipes</h2>
<ol>
<li>Whipped-Cream Corn Salad</li>
<li>Classic Grilled Corn on the Cob</li>
...
</ol>
<div>
</MainLayout>
)
}
You can read more and play around with the example code from React's official documents.
It is the basic concept of React, so before you start to build something big, I suggest to follow this docs first or find some series of React tutorials for beginner, they will explain key concepts of React so you would not save more time.
You need to use react-router-dom to navigate when you click other MenuItem. Create your own RouterProvider and put it in the Content.
<Content>
<RouterProvider router={router}>
</Content>
EDIT
Now you have two way to navigate to your Component. First is using Link and set it to your label.
{
label: <Link title="Rice" to="/rice">Rice</Link>,
key: 'rice',
}
Second way is hook useNavigate
const navigate = useNavigate();
const onClick = (e)=>{navigate(`/${e.key}`)}
//Add to Menu
<Menu
onClick={onClick}
//other props
/>

When using a functional component, why do parent state changes cause child to re-render even if context value doesn't change

I'm switching a project from class components over to functional components and hit an issue when using a context.
I have a parent layout that contains a nav menu that opens and closes (via state change in parent). This layout component also contains a user property which I pass via context.
Everything worked great before switching to the functional components and hooks.
The problem I am seeing now is that when nav menu is opened from the layout component (parent), it is causing the child component to re-render. I would expect this behavior if the context changed, but it hasnt. This is also only happening when adding useContext into the child.
I'm exporting the children with memo and also tried to wrap a container with the children with memo but it did not help (actually it seemed to cause even more renders).
Here is an outline of the code:
AppContext.tsx
export interface IAppContext {
user?: IUser;
}
export const AppContext = React.createContext<IAppContext>({});
routes.tsx
...
export const routes = <Layout>
<Switch>
<Route exact path='/' component={Home} />
<Route exact path='/metrics' component={Metrics} />
<Redirect to="/" />
</Switch>
</Layout>;
Layout.tsx
...
const NavItems: any[] = [
{ route: "/metrics", name: "Metrics" }
];
export function Layout({ children }) {
const aborter = new AbortController();
const history = useHistory();
const [user, setUser] = React.useState<IUser>(null);
const [navOpen, setNavOpen] = React.useState<boolean>(false);
const [locationPath, setLocationPath] = React.useState<string>(location.pathname);
const contextValue = {
user
};
const closeNav = () => {
if (navOpen)
setNavOpen(false);
};
const cycleNav = () => {
setNavOpen(prev => !prev);
};
React.useEffect(() => {
Fetch.get("/api/GetUser", "json", aborter.signal)
.then((user) => !aborter.signal.aborted && !!user && setUser(user))
.catch(err => err.name !== 'AbortError' && console.error('Error: ', err));
return () => {
aborter.abort();
}
}, []);
React.useEffect(() => {
return history.listen((location) => {
if (location.pathname != locationPath)
setLocationPath(location.pathname);
})
}, [history]);
const navLinks = NavItems.map((nav, i) => <li key={i}><Link to={nav.route} onClick={closeNav}>{nav.name}</Link></li>);
return (
<div className="main-wrapper layout-grid">
<header>
<div className="header-bar">
<div className="header-content">
<div className="mobile-links-wrapper">
<ul>
<li>
<div className="mobile-nav-bars" onClick={cycleNav}>
<Icon iconName="GlobalNavButton" />
</div>
</li>
</ul>
</div>
</div>
</div>
<Collapse className="mobile-nav" isOpen={navOpen}>
<ul>
{navLinks}
</ul>
</Collapse>
</header>
<AppContext.Provider value={contextValue} >
<main role="main">
{children}
</main>
</AppContext.Provider>
<a target="_blank" id="hidden-download" style={{ display: "none" }}></a>
</div>
);
}
Metrics.tsx
...
function Metrics() {
//Adding this causes re-renders, regardless if I use it
const { user } = React.useContext(AppContext);
...
}
export default React.memo(Metrics);
Is there something I am missing? How can I get the metrics component to stop rendering when the navOpen state in the layout component changes?
Ive tried memo with the switch in the router and around the block. I've also tried moving the contextprovider with no luck.
Every time your Layout component renders, it creates a new object for the contextValue:
const contextValue = {
user
};
Since the Layout component re-renders when you change the navigation state, this causes the context value to change to the newly created object and triggers any components depending on that context to re-render.
To resolve this, you could memoize the contextValue based on the user changing via a useMemo hook and that should eliminate the rendering in Metrics when the nav state changes:
const contextValue = React.useMemo(() => ({
user
}), [user]);
Alternatively, if you don't really need the object, you could simply pass the user as the context value directly:
<AppContext.Provider value={user}>
And then access it like:
const user = React.useContext(AppContext);
That should accomplish the same thing from an unnecessary re-rendering point of view without the need for useMemo.

How can I change an image dynamically in Ionic React?

I'm pretty new to React and TypeScript, and I ran into this problem:
In my UI I'm using several decorative graphics as follows:
import Kitten from './img/Kitten.png';
<img className="Image" src={Kitten} />
Now, I have a dark-mode toggle. When it fires, I want to replace all images with their appropriate dark-mode version. I was thinking about something like this:
import Kitten from './img/Kitten.png';
import DarkKitten from './img/DarkKitten.png';
//gets called when dark mode is toggled on or off
const darkModeToggleFunc = () => {
document.querySelectorAll('.Image').forEach(element => {
if(element.src.includes("Dark")) {
element.src = element.src.replace("Dark", "");
} else{
element.src = "Dark" + element.src;
}
});
}
<img className="Image" src={Kitten} />
Now, in React I have two problems: the .src-attribute is unknown because element is not necessarily an image and the second problem is: I don't assign URIs as src but the variable from the import. So there isn't really a string I can change... If I'm informed correctly, React uses Base64 for images specified this way.
How could I achieve my goal in React?
Edit: App.tsx
//bunch of imports
const App: React.FC = () => {
return (
<IonApp>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<IonRouterOutlet id="main">
<Route path="/page/:name" component={Page} exact />
<Redirect from="/" to="/page/Production" exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
};
export default App;
First things first when it comes to react you dont directly go and change things in the document level, you update the virtual DOM and let react take care of the rest.
You scenario is on changing the theme of the app, this answer is on using React context to change theme and use images appropriately.
First you create a Context which will hold the theme value
const AppContext = createContext({
theme: "light",
setTheme: (theme) => {}
});
Here we are going to use a state variable for simplicity, you can use anything you prefer.
Heres the app.js file
export default function App() {
const [theme, setTheme] = React.useState("light");
const themeState = { theme, setTheme };
return (
<AppContext.Provider value={themeState}>
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<ImageViewer />
<DarkModeSwitch />
</div>
</AppContext.Provider>
);
}
Here we set the theme value in the state and set the context to that, the setTheme can be used to update the theme from any component that is in the tree. in your case the darkmodeswitch, here we toggle the value
const DarkModeSwitch = () => {
const { theme, setTheme } = useContext(AppContext);
const darkModeToggle = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<div>
<input
type="checkbox"
checked={theme === "light"}
onChange={() => darkModeToggle()}
/>
</div>
);
};
Coming to your main requirement, the images, lets use a common files for images with the contents
export const Kitten ="image source 1";
export const KittenDark ="image source 2";
You simply set the image based on the theme like below
import { Kitten, KittenDark } from "./images";
export default function ImageViewer() {
const { theme } = useContext(AppContext);
return (
<img
alt="kitten"
style={{ height: 50, width: 100 }}
src={theme === "light" ? Kitten : KittenDark}
/>
);
}
as you can see everything is connected via the context and once you update the context you can see the images change.
You can see a working version here
https://codesandbox.io/s/react-theme-switch-3hvbg
This is not 'THE' way, this is one way of handling the requirement, you can use things like redux etc

React Smooth Scroll into specific location in my reusable component?

So I originally created a smooth scroll effect with regular HTML css and js, but I'm trying to convert it into react, but I'm not sure if I am doing it properly.
I have a Navbar component and just set the path
<NavLinks to='/#posts'>Posts</NavLinks>
Then I have my Main Content sections which are reusable components that receive data and display a different design based on what data I pass in.
function InfoSection({
lightBg,
id
}) {
return (
<>
<div
id={id} //Passed in the id value
className={lightBg ? 'home__hero-section' : 'home__hero-section darkBg'}
>
// Rest of my JSX is inside here
</div>
</>
);
}
Then in my Data file I'll pass in the values
export const homeObjThree = {
id: 'posts',
lightBg: true,
}
Then I display my reusable components in my index.js page
function Home() {
return (
<>
<InfoSection {...homeObjThree} />
</>
);
}
Then I import that into my App.js file
function App() {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
setIsOpen(!isOpen);
};
return (
<Router>
<Sidebar isOpen={isOpen} toggle={toggle} />
<Navbar toggle={toggle} />
<Home />
<Footer />
</Router>
);
}
So when I inspect, it shows the Home HTML with an id="posts" which I set from my data file, but when I click on the Post nav menu item it doesn't scroll down to the section. Nothing actually happens, so I can't tell if my approach even works for padding in ids into my data file or if I would need to write scroll functions to implement it.

Resources