Rendering different html in reusable components - reactjs

So I'm trying to make a reuseable component, which takes in an array and a variable, then i want to map through that array and return html.
But when i reuse this component, the html wont necesarly be the same each time i use it. For example:
Home Component
<div className='home'>
<Mappedarray array={list} pattern={pattern}/>
</div>
Account Component
<div className='account'>
<Mappedarray array={users} pattern={pattern}/>
</div>
function Mappedarray(props) {
const {array, pattern} = props
const arrayrow = array?.map(el=>{
return VARIABLE_HTML
})
}
So this is the basic set up, now for the VARIABLE_HTML, I want to return different html elements, for example, in the Home Component I want to return
<Link to={el.link}>
<p>{el.title}</p>
<i className={el.src}'></i>
</Link>
But for the User Component, I want to return
<div className='usercont'>
<img src={el.src}/>
<p>{el.title}</p>
</div>
I've been using a solution like passing a boolean variable to the component to determine what html should be used, but the component will get very messy and doesn't seem like a good solution.
For example, in the Home Component I would pass down:
<Mappedarray array={list} pattern={pattern} home={true} />
Then in the Mappedarray Component I would do
function Mappedarray(props) {
const {array, pattern, home, user} = props
const arrayrow = array?.map(el=>{
return <>
{
home?
<Link to={el.link}>
<p>{el.title}</p>
<i className={el.src}'></i>
</Link>
:user?
<div className='usercont'>
<img src={el.src}/>
<p>{el.title}</p>
</div>
:ANOTHER_VAR?
...
}
</>
})
}
Therefore, by doing it like this it would get very messy and disorganized, looking to a more dynamic way of doing this

Well since you are mapping trough same array and want different result maybe reusable component is not for this the best case. But if you want to have this united into one component like this you can just add a flag isLink to your reusable component and you are done:
function Mappedarray(props) {
const { array, pattern, isLink } = props;
const arrayrow = array?.map((el) => {
return isLink ? (
<Link to={el.link}>
<p>{el.title}</p>
<i className={el.src}></i>
</Link>
) : (
<div className="usercont">
<img src={el.src} />
<p>{el.title}</p>
</div>
);
});
}
Than this would be usage of that component in two cases:
Home Component
<div className='home'>
<Mappedarray array={list} pattern={pattern} isLink={true}/>
</div>
Account Component
<div className='account'>
<Mappedarray array={users} pattern={pattern} isLink={false}/>
</div>
NOTE
If you just put isLink with no ={true} it will be implicitly true. But for this example i added it explicitly

You can accept a render function as a prop. But if you are doing that then your Mappedarray isn't doing much of anything.
function Mappedarray({ array = [], render }) {
return (
<div>{array.map(render)}</div>
);
}
You can define render components for various types. Make sure that you are setting a key property since we will use this as a callback for .map.
const MyLink = ({ link, title, src }) => (
<Link to={link} key={link}>
<p>{title}</p>
<i className={src}></i>
</Link>
)
You would call Mappedarray by passing the function component as the render prop. Your array prop would be an array of props for that component.
const Test = () => {
return (
<Mappedarray
array={[{ link: "/", title: "home", src: "/images/home.jpg" }]}
render={MyLink}
/>
)
}
With Typescript Types
You could also tweak this slightly to pass the array index as a prop to the render component instead of passing it as a second argument. This version allows for both class components and function components to be passed to render.
function Mappedarray<T>({ array = [], render: Render }: Props<T>) {
return (
<div>{array.map((props, index) => (
<Render {...props} index={index} />
))}</div>
);
}
With Typescript Types

Related

(Theme Toggle) How can I make className sync dynamically to active localStorage value from its key when toggled in React

className "page" needs to be modified to "page light-theme" or "page dark-theme" on toggle through local storage key "theme-color" with values of light-theme and dark-theme.
The active key value does change in local Storage but updates only show if the pages is refreshed. I need the changes to sync on toggle
Page to be changed
export default function Page({children}){
return(
<div className={`page ${localStorage.getItem('theme-color')}`}>
{/* <div className= "page"> */}
{children}
</div>
)
}
h3 inner text needs to change dynamically depending on the active value from key "theme-color" in local storage. I have place the variable "themeOpener" in between h3 tag. No changes take place
heres is my code
export default function Body() {
let themeOpener;
if (`${localStorage.getItem('theme-color','light-theme')}`) {
themeOpener = "🧛🏼Ahh the light it burns! Please use toggle, I prefer dark mode!";
} else {
themeOpener = "I learnt to design in React and im hooked 🤩";
}
return (
<div className="body">
{/* <h3 id="opener">I learnt to design in React and im hooked 🤩</h3> */}
<h3 id="opener">{themeOpener}</h3>
</div>
);
}
This is code for my toggle where local storage is created
const ToggleMode = () => {
// state
const [isLight, setIsLight] = useState(false);
// effect
useEffect(() => {
// check local storage
const CurrentTheme = localStorage.getItem("theme-color");
if (CurrentTheme === "light-theme") {
setIsLight(true);
} else {
setIsLight(false);
}
console.log(useEffect);
}, []);
const ToggleChecked = () => {
// logic
if (isLight) {
localStorage.setItem("theme-color", "dark-theme");
setIsLight(false);
} else {
localStorage.setItem("theme-color", "light-theme");
setIsLight(true);
}
console.log(ToggleChecked);
};
return (
<div className="toggle--container">
<input
type={"checkbox"}
id="toggle"
className="toggle--checkbox"
checked={isLight}
onChange={ToggleChecked}
/>
<label htmlFor="toggle" className="toggle--label">
<span className="toggle--label-background"></span>
</label>
<div className=""></div>
</div>
);
};
export default ToggleMode;
Yes, this is natural according to your code. Whenever toggling, ToggleMode component will only be re-rendered by changed state value isLight.
But your Body and Page component which are supposed to be ToggleMode's parent will not be re-rendered. Because their props or states never changed by ToggleChecked().
To get it done working, you need to do something to re-render parents in Page and Body component.
How? You need to create a state value in those components or create IsLight and setIsLight at the top level component. And then these two would be drilled into ToggleMode.
Something like followings.
export default function Page({children}){
// state
const [isLight, setIsLight] = useState(false);
return(
<div className={`page ${localStorage.getItem('theme-color')}`}>
{/* <div className= "page"> */}
<Body isLight={isLight} setIsLight={setIsLight} />
{children}
</div>
)
}
export default function Body({isLight, setIsLight}) {
return (
<div>
<ToggleMode isLight={isLight} setIsLight={setIsLight} />
</div>
);
}
export default function ToggleMode ({isLight, setIsLight}){
// This is not needed anymore.
// const [isLight, setIsLight] = useState(false);
return (<>Your toggle code...</>)
}
P.S. Don't you think this is quite irritating? To avoid prop drilling, we use state management utilities such as react context API or 3rd party libraries such as Redux.

Typescript React - use MouseEvents without passing it to child element

Can I use Mouse Events on whole React Element or I have to pass it to child element to get it work? After several functions components where I passed my handleEvent function I want to know if it's possible without getting a TypeScript error. My code is really simple
<Tile onHover={handleHover} name="Random name"/>
and Tile component
export const Tile: React.FC<{ name: string }> = ({ name }) => {
return (
<div className="tile-wrapper">
<h1 className="tile-header>
{name}
</h1>
</div>
)
}
What you want is to combine React.DOMAttributes<HTMLElement> with your custom props using an intersection. That will avail all the DOM events to your custom component, without having to manually pass it in.
export const Tile: React.FC<{ name: string } & React.DOMAttributes<HTMLElement>> = ({ name, ...events }) => {
return (
<div className="tile-wrapper" {...events}>
<h1 className="tile-header>
{name}
</h1>
</div>
)
}
As your Title component does not expect an onHover prop, it will just be ignored. The event you want to use is rather onMouseOver, which works on most HTML elements (according to documentation):
All HTML elements, EXCEPT: <base>, <bdo>, <br>, <head>, <html>, <iframe>, <meta>, <param>, <script>, <style>, and <title>
What you could do is the following, in the Title component:
export const Tile: React.FC<{ name: string, onHover: SomeType }> = ({ name, onHover }) => {
return (
<div className="tile-wrapper" onMouseOver={onHover}>
<h1 className="tile-header>
{name}
</h1>
</div>
)
}

React wrapper component (HOC) does not re-render child component when it's props are changing

My wrapper component has this signature
const withReplacement = <P extends object>(Component: React.ComponentType<P>) =>
(props: P & WithReplacementProps) => {...}
Btw, full example is here https://codepen.io/xitroff/pen/BaKQNed
It's getting original content from argument component's props
interface WithReplacementProps {
getContent(): string;
}
and then call setContent function on button click.
const { getContent, ...rest } = props;
const [ content, setContent ] = useState(getContent());
I expect that content will be replaced everywhere (1st and 2nd section below).
Here's the part of render function
return (
<>
<div>
<h4>content from child</h4>
<Component
content={content}
ReplaceButton={ReplaceButton}
{...rest as P}
/>
<hr/>
</div>
<div>
<h4>content from wrapper</h4>
<Hello
content={content}
ReplaceButton={ReplaceButton}
/>
<hr/>
</div>
</>
);
Hello component is straightforward
<div>
<p>{content}</p>
<div>
{ReplaceButton}
</div>
</div>
and that's how wrapped is being made
const HelloWithReplacement = withReplacement(Hello);
But the problem is that content is being replaced only in 2nd part. 1st remains untouched.
In the main App component I also replace the content after 20 sec from loading.
const [ content, setContent ] = useState( 'original content');
useEffect(() => {
setTimeout(() => {
setContent('...too late! replaced from main component');
}, 10000);
}, []);
...when I call my wrapped component like this
return (
<div className="App">
<HelloWithReplacement
content={content}
getContent={() => content}
/>
</div>
);
And it also has the issue - 1st part is updating, 2nd part does not.
It looks like you are overriding the withReplacement internal state with the external state of the App
<HelloWithReplacement
content={content} // Remove this to stop overriding it
getContent={() => content}
/>
Anyway it looks weird to use two different states, it is better to manage your app state in only one place

Reactjs not updating dom classes after splicing state

Hello I am new to react and have a question with changing classes and animation onClick.
I am trying to move up and down only with css classes that I add or remove to an array that is in my className.
app.js i have this in state
updown: ["containerMG"],
and here is how i render my components in app.js
render() {
return (
<div>
<div className="movies-container">
{this.state.filmovi.map((film, index) => {
return <Film
naslov={film.naslov}
naslovnaSlika={film.naslovnaSlika}
key={film.id}
openFilm={() => this.injectFilm(index)}/>
})}
</div>
<Gallery />
<ContainerMG
selectedFilm={this.state.selectedFilm}
klasa={this.state.updown}
zatvori={this.closePreview}/>
</div>
);
}
my component looks like this
const ContainerMG = (props) => {
return (
<div className={props.klasa.join(' ')}>
<img onClick={props.zatvori} src="xxx" alt="close" className="close-popup" />
<p>{props.selectedFilm.naslovFilma}</p>
</div>
)
}
this is how the div moves up
injectFilm = (filmIndex) => {
this.state.updown.push("position-top");
const selectedFilm = this.state.filmovi.find((film, index) => index === filmIndex)
this.setState((prevState) => ({
selectedFilm
}))
}
this is how i tried to move it down
closePreview = () => {
this.state.updown.splice(1);
}
i am pretty sure i should not change the state directly, and also when i change it to remove the "position-top" class the dom doesn't reload.
thank you for all the help in advance and if i didn't show something that you need please do write.
You're right, you should never change the state directly like that, but rather, use a setState() method. Doing this.state.updown.push("position-top"); will mutate it. Let's say you want to remove whatever is last from the array, you could do something like:
const {updown} = this.state;
updown.pop();
this.setState({updown: updown});
Which would cause a re-render. Treat the state as if it were immutable.

Passing data between components using reactjs

I have a div like following.
<div className="horiz_center" onClick={this.textType.bind(this,'text')}>
<img src={StarIcon} className="post_type_icon"/>
<a className="post_type_text">Text</a>
</div>
I can get the value with this function
textType(postType) {
this.setState({postType});
}
My question is if i want to use postType in another component, how can i pass it to that component?
const {postType} = this.state;
<MyComponent postType={postType} />
And in your component ( MyComponent ) access your properties like
this.props.postType

Resources