How can I change an image dynamically in Ionic React? - reactjs

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

Related

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
/>

React Beginner - Trouble understanding useContext for dark mode

I'm completing an online program to learn ReactJS. After going over useState we are now learning useContext. Below I'll go over my current understanding of how useContext works, and where I'm facing trouble.
The goal is a simple page with a light/dark mode switch
What I currently understand as the "steps" to using useContext:
Import and initialize createContext
Wrap child components with Provider
Import useContext hook from React so we can use the Context in child components
Access the user Context in desired component(s)
But I'm facing an issue with understanding the code block below
This is the solution to a file named ThemeContext.js.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(undefined);
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider
value={{
theme,
toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"),
}}
>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
This is the solution to App.js.
import "./App.css";
import { ThemeProvider, useTheme } from "./ThemeContext";
import Switch from "./Switch";
const Title = ({ children }) => {
const { theme } = useTheme();
return (
<h2
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</h2>
);
};
const Paragraph = ({ children }) => {
const { theme } = useTheme();
return (
<p
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</p>
);
};
const Content = () => {
return (
<div>
<Paragraph>
We are a pizza loving family. And for years, I searched and searched and
searched for the perfect pizza dough recipe. I tried dozens, or more.
And while some were good, none of them were that recipe that would
make me stop trying all of the others.
</Paragraph>
</div>
);
};
const Header = () => {
return (
<header>
<Title>Little Lemon 🍕</Title>
<Switch />
</header>
);
};
const Page = () => {
return (
<div className="Page">
<Title>When it comes to dough</Title>
<Content />
</div>
);
};
function App() {
const { theme } = useTheme();
return (
<div
className="App"
style={{
backgroundColor: theme === "light" ? "white" : "black",
}}
>
<Header />
<Page />
</div>
);
}
function Root() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
);
}
export default Root;
Finally, this is the solution to index.js
import "./Styles.css";
import { useTheme } from "../ThemeContext";
const Switch = () => {
const { theme, toggleTheme } = useTheme();
return (
<label className="switch">
<input
type="checkbox"
checked={theme === "light"}
onChange={toggleTheme}
/>
<span className="slider round" />
</label>
);
};
export default Switch;
My questions begin here
Instead of directly wrapping children with Provider, they instead create ThemeProvider that then returns ThemeContext.Provider. Why is this? and why is { children } necessary as seen in App.js along with the return ThemeContext return statement?
This exercise goes beyond what I believe was taught in the lesson, so I could have some holes to fill in my knowledge as far as using { children } along with the use of ThemeProvider. Normally it's demonstrated as <ThemeContext.Provider> wrapping children on the inside. In App it looks like they don't do this, but it's done in the Root, and maybe since they're wrapping App that's why { children } is indicated? I'm not certain about this and I'd just like to know why things were done specifically like this (again, this is unlike what was demonstrated in past exercises). First post, thanks in advance.
EDIT: After looking more into this issue I'm starting to come around and understand how they came up with this solution. One of the few things they didn't do previously that was used in this example was the use of ({ children }). This caused confusion for me at first, but I've come closer to understanding its usage. For example, its use in the Paragraph component:
const Paragraph = ({ children }) => {
const { theme } = useTheme();
return (
<p
style={{
color: theme === "light" ? "black" : "white",
}}
>
{children}
</p>
);
};
Which is later referenced in the Content component as such:
<Paragraph>
We are a pizza loving family. And for years, I searched and searched and
searched for the perfect pizza dough recipe. I tried dozens, or more.
And while some were good, none of them were that recipe that would
make me stop trying all of the others.
</Paragraph>
This simply means to take the children of the Paragraph component and return the information styled as such. Whatever comes inside of Paragraph, in this case a block of text, was returned with the intended style. I thought of deleting this post but maybe it will help someone else. Not sure if adding more about what I learned here would be excessive, and I'm still wrapping my head around the rest of the issue so documenting here isn't my top priority as of now.
React Context is a relatively new feature that the React team introduced to enable an alternative to holding state in a component and prop drilling. The context provider is a node that holds a value, which can be accessed by any node rendered under it (any children or children of children) via the createContext() with useContext or Context.Consumer. When the value of the Provider changes, any component subscribed with useContext or rendered by Consumer will be rerendered.
There is no practical difference between using the provider directly in index.js. What matters is who is rendered under the provider. Keep in mind that JSX's <ThemeProvider> is in reality a call to React.createElement with App in the children argument.
Why create a ThemeProvider? It packages the provider with state. The alternative would be to use the provider directly in Root and create useState there, but it is inflexible and not reusable.
const ThemeContext = createContext(undefined);
function Root() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider
value={{
theme,
toggleTheme: () => setTheme(theme === "light" ? "dark" : "light"),
}}
>
<App />
</ThemeContext.Provider>
);
}
Note that App is still the children of the provider.

Cannot set a Loader while big component renders

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.

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.

Is there a better/another way to create list in react?

In React, to render a list you generally map over and render elements like this:
list.map((itm, idx) => <li>{itm}</li>)
Can I refactor this to a component that is generic enough to handle different element types but still do the mapping for me? Aka,
<EasyList data={list} /> // tag would be "li" by default
<EasyList data={list} tag="p" /> // a customized tag name
What would be the best way to implement something like this?
You can achieve exactly what you are describing in the way you are describing it. The implementation of EasyList could look something like this
const EasyList = ({data, tag: Elem = 'li'}) => {
return data.map( (item, idx) => <Elem key={idx}>{item}</Elem>)
}
and usage would look like
<EasyList data={list} /> // tag would be "li" by default
<EasyList data={list} tag="p" /> // a customized tag name
Demo to play with
There are multiple ways you could implement this, so depending on needs you could go the route of a render prop, where you define the exact elements you need. Really it depends on what kind of control / granularity you want.
<EasyList data={seed} render={ (item, idx) => <li>{item}</li>} />
and the implementation with a render prop would look like
const EasyList = ({data, render}) => {
return data.map(render)
}
This answer isn't much different than John Ruddell's, but I had already completed it before seeing his.
You can make this EasyList component as sophisticated as you need it to be. My example below shows that you can have it control the parent component (ul by default) and that the component types passed can be html element types (e.g. ul, li, p) or custom components such as DivWithHeaderText in my example.
import React from "react";
import ReactDOM from "react-dom";
const EasyList = ({ data, ListComponent = "ul", ItemComponent = "li" }) => {
return (
<ListComponent>
{data.map(item => (
<ItemComponent>{item}</ItemComponent>
))}
</ListComponent>
);
};
const myItems = ["Here", "are", "my", "list", "items"];
const DivWithHeaderText = ({ children }) => {
return (
<div style={{ border: "solid black 1px" }}>
<div>Here is my header</div>
<hr />
{children}
</div>
);
};
function App() {
return (
<>
<EasyList data={myItems} />
<EasyList
data={myItems}
ListComponent={DivWithHeaderText}
ItemComponent="p"
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Resources