React compound component without looping Children.map - reactjs

Is there a way to use compound component but without using Children.map?
Here my example.
export class Dashboard extends Component {
static Header = ({ children }) => {
return <header className="header">{children}</header>;
};
static Sidebar = ({ children }) => {
return <aside className="aside">{children}</aside>;
};
static Main = ({ children }) => {
return <main className="main">{children}</main>;
};
static Footer = ({ children }) => {
return <footer className="footer">{children}</footer>;
};
render() {
const { children } = this.props;
const elements = Children.toArray(children);
return (
<div>
{cloneElement(elements[0])}
<div>
{cloneElement(elements[1])}
{cloneElement(elements[2])}
<div>
{cloneElement(elements[3])}
</div>
</div>
</div>
);
}
}
Usage example
<Dashboard>
<Dashboard.Sidebar>THIS IS Sidebar</Dashboard.Sidebar>
<Dashboard.Header>THIS IS HEADER</Dashboard.Header>
<Dashboard.Main>THIS IS MAIN</Dashboard.Main>
<Dashboard.Footer>THIS IS FOOTER</Dashboard.Footer>
</Dashboard>
But as you can see, it is based on array index which is not a good solution.
Is there something like
return (
<div>
{ cloneElement(Children.byName('Sidebar'))}
<div>
{cloneElement(Children.byName('Header'))}
{cloneElement(Children.byName('Main'))}
<div>
{cloneElement(Children.byName('Footer'))}
</div>
</div>
</div>
);

No, but you can just use javascript for it:
export class Dashboard extends Component {
render() {
const { children } = this.props;
const [sidebar, header, main, footer] = Children.toArray(children);
return (
<div>
{sidebar}
<div>
{header}
{main}
<div>{footer}</div>
</div>
</div>
);
}
}
Either way, it's an error-prone API where the user must know the children order, for example, changing the order breaks the component:
<Dashboard>
<Dashboard.Footer>THIS IS FOOTER</Dashboard.Footer>
<Dashboard.Main>THIS IS MAIN</Dashboard.Main>
<Dashboard.Sidebar>THIS IS Sidebar</Dashboard.Sidebar>
<Dashboard.Header>THIS IS HEADER</Dashboard.Header>
</Dashboard>
I would recommend passing the components via props:
<Dashboard footer={footerComponent}/>
// Usage inside Dashboard
<Dashboard.Footer>
{footerComponet}
</Dashboard.Footer>
// Or lazy render
<Dashboard footer={() => footerComponent}/>
// Usage inside Dashboard
const FooterComponent = footerComponent
<Dashboard.Footer>
<FooterComponent/>
</Dashboard.Footer>

Related

Modify class name of react element programmatically

Is there a native way to add a style class name to a react element passed as a property WITHOUT using jQuery or any 3rd-party libraries.
The following example should demonstrate what I'm trying to do. Note, react class names are made up.
Edit: The point is to modify the class name of a react element that is passes as a property to the Books class! The Books class needs to modify the class name. Apparently, it does not have access to Authors class's state to use within Authors class.
File authors.js
class Authors {
render() {
return (
<ul>
<li>John Doe</li>
<li>Jane Doe</li>
</ul>
);
}
}
File shelf.js
class Shelf {
render() {
return (
<div>
<Books authors={<Authors/>}/>
</div>
);
}
}
File books.js
class Books {
this.props.authors.addClass('style-class-name'); <- HERE
render() {
return (
<div>
{this.props.authors}
</div>
);
}
}
Potentially need more context, but in this kind of scenario, I would use state to dynamically add/remove a class. A basic example would be:
const App = () => {
const [dynamicClass, setDynamicClass] = useState("");
return (
<div className={`normalClass ${dynamicClass}`}>
<button onClick={() => setDynamicClass("red")}>Red</button>
<button onClick={() => setDynamicClass("green")}>Green</button>
<button onClick={() => setDynamicClass("")}>Reset</button>
</div>
);
};
The state changes schedule a re-render, hence you end up with dynamic classes depending on the state. Could also pass the class in as a property, or however you want to inject it into the component.
React elements do have an attribute called className. You can use that to set CSS classes to your component. You can pass static data (strings) or dynamic ones (basically calculated ones):
<div className="fixedValue" />
<div className={fromThisVariable} />
Keep in mind, that you have to pass down your className, if you wrap native HTML elements in a component:
class Books {
render() {
const {
authors,
// other represents all attributes, that are not 'authors'
...other
}
return (
<div {...other}>
{this.props.authors}
</div>
);
}
}
If you want to add data to your authors attribute (which I assume is an array), you could implement a thing like the following:
let configuredAuthors = this.props.authors.map((author) => ({
return {
...author,
className: `${author.firstName}-${author.lastName}`
}
}))
Keep in mind, that either way, you have to manually assign this className property in your child components (I guess an Author component)
To handle updates from a child component, use functions: https://reactjs.org/docs/faq-functions.html
Full example:
import React from "react";
class Shelf extends React.Component {
render() {
const authors = [
{
firstName: "John",
lastName: "Tolkien"
},
{
firstName: "Stephen",
lastName: "King"
}
];
return (
<div>
<Books authors={authors} />
</div>
);
}
}
const Books = ({authors, ...other}) => {
const [configuredAuthors, setAuthors] = React.useState(authors)
const updateClassName = (authorIndex, newClassName) => {
const newAuthors = [...configuredAuthors]
newAuthors[authorIndex] = {
...configuredAuthors[authorIndex],
className: newClassName
}
setAuthors(newAuthors)
}
return (
<ul {...other}>
{configuredAuthors.map((author, index) => {
return <Author key={index} author={author}
index={index}
updateClassName={updateClassName}
/>;
})}
</ul>
);
}
const Author = ({ author, index, updateClassName, ...other }) => {
const [count, setCount] = React.useState(0);
return (
<li className={author.className} {...other}>
<span>{`${author.firstName} ${author.lastName}`}</span>
<button
onClick={() => {
updateClassName(index, `author-${count}`);
setCount(count + 1);
}}
>
update Class ()
{`current: ${author.className || '<none>'}`}
</button>
</li>
);
};
export default function App() {
return <Shelf />;
}

Using refs and .reduce scroll to the id of selected element with react with useState

sorry if the title doesn't make much sense.
I've been refactoring my code from this.state to useState, and I finally got things working except for the refs...
In my original code I was making individual axios calls and using this.state along with this refs code:
const refs = response.data.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
but now I refactored my axios call to .all:
const getData = () => {
const getSunSigns = axios.get(sunSignAPI);
const getDecans = axios.get(decanAPI);
const getNums = axios.get(numbersAPI);
axios.all([getSunSigns, getDecans, getNums, refs]).then(
axios.spread((...allData) => {
const allSunSigns = allData[0].data;
const getAllDecans = allData[1].data;
const getAllNums = allData[2].data;
setSunSigns(allSunSigns);
setDecanSign(getAllDecans);
setNumerology(getAllNums);
})
);
};
useEffect(() => {
getData();
}, []);
so the response.data.reduce doesn't work cuz I'm not using 'response'.
I've tried several things but none worked.. unfortunately I deleted all the previous code but this is what I currently have, which works but obviously only takes one api:
const refs = sunSigns.reduce((acc, value) => {
acc[value.id] = createRef();
return acc;
}, {});
onClick = (id) => {
refs[id].current.scrollIntoView({
behavior: "smooth",
});
};
from the research I've done and the code I've tried I'm sure I'd have to map through the apis and then maybe use the reduce(???).. but I'm really not entirely sure how to go about it or how to rephrase my google search to get more accurate results.
what I'm trying to do specifically: on certain pages an extra nav bar appears with the symbol of a specific sign/number. the user can click on one and it'll scroll to that specific one. I'm going to have several pages with this kind of feature so I need to dynamically set refs for each api.
any help or guidance will be highly appreciated!!
edit**
the above codes are in my Main component and this is where I'm setting the refs:
return (
<div className='main'>
<div className='main__side-container'>
<SideBar />
<div className='main__card-container'>
<Card
sunSigns={sunSigns}
numerology={numerology}
decanSign={decanSign}
refs={refs}
/>
</div>
</div>
<div className='main__bottom-container'>
<BottomBar
sunSigns={sunSigns}
numerology={numerology}
onClick={onClick}
/>
</div>
</div>
);
}
then this is the card:
export default function Card({ sunSigns, decanSign, refs, numerology }) {
return (
<>
<div className='card'>
<Switch>
<Route path='/astrology/western/zodiac'
render={(routerProps) => <Zodiac refs={refs} sunSigns={sunSigns} />}
/>
<Route path='/numerology/pythagorean/numbers'
render={(routerProps) => <NumberPage refs={refs} numerology={numerology} />}
/>
</Switch>
</div>
</>
);
}
and then this is the Zodiac page:
export default function Zodiac({ sunSigns, refs }) {
return (
<>
<div className='zodiac__container'>
<TitleBar text='ZODIAC :' />
<div className='card-inset'>
<div className='container-scroll'>
<SunSignsList sunSigns={sunSigns} refs={refs} />
</div>
</div>
</div>
</>
);
}
and the SunSignsList component:
export default function SunSignsList({ sunSigns, refs }) {
return (
<>
<div className='sunsignsitemlist'>
<ul>
{sunSigns.map(sign => {
return (
<SunSigns
refs={refs}
key={sign.id}
id={sign.id}
sign={sign.sign}
/>
);
})}
</ul>
</div>
</>
);
}
and the SunSigns component:
export default function SunSigns({
id,
sign,
decans,
refs
}) {
return (
<li ref={refs[id]}>
<section className='sunsigns'>
<div className='sunsigns__container'>
<div className='sunsigns__header'>
<h3 className='sunsigns__title'>
{sign}
{decans}
</h3>
<h4 className='sunsigns__symbol'>{symbol}</h4>
</section>
</li>
);
}
the above code is where my ref code is currently accessing correctly. but the end goal is to use them throughout several pages and comps in the same manner.
You can create three different objects holding the ref data for each list or if the id is same you can generate a single object which holds all the list refs.
const generateAllRefsObj = (...args) => {
const genSingleListRefsObj = (acc, value) => {
acc[value.id] = createRef();
return acc;
}
return args.reduce((acc, arg) => ({ ...arg.reduce(genSingleListRefsObj, acc), ...acc }), {})
}
Usage
const allRefs = generateAllRefsObj(sunSigns,decanSign,numerology)

React useState hook - component is rendered twice

I wonder why my component SearchResults is rendered twice.
In MainPage component I want to pass offers to child component SearchResults:
const mainPage = () => {
const [offers, setOffers] = useState(null);
useEffect(() => {
onInitOffers();
}, [])
const onInitOffers = () => {
axios.get('/offers')
.then(response => {
setOffers(response.data);
})
.catch(error => {
console.log(error);
})
}
const searchResults = (
<SearchResults
searchedOffers={offers}
/>
);
return (
<Aux>
<div className={classes.container}>
<div className={classes.contentSection}>
{searchResults}
</div>
</div>
</Aux>
)
}
export default mainPage;
Why the component SearchResults is rendered twice? How to correctly pass offers to child component using hooks?
In my child component SearchResults I have to add if condition to avoid error map is not a function:
const searchResults = props => {
useEffect(() => {
console.log("RENDER");
console.log(props.searchedOffers) --> null for the first time
}, [props.searchedOffers]);
let offers = null;
if (props.searchedOffers !== null) { --> props.searchedOffers is not null after the second render
offers = props.searchedOffers.map(offer => {
return (
<Grid key={offer.id}>
<SearchResult key={offer.id} offer={offer}/>
</Grid>
)
});
}
It's rendered twice because, when the element mounts, you set offers to null. If you want to make sure you only render the SearchResults component when offers isn't null, you can do something like:
return (
<Aux>
<div className={classes.container}>
<div className={classes.contentSection}>
{offers && <SearchResult searchedOffers={offers} />}
</div>
</div>
</Aux>
)
If you want to be super sure offers is an array, you can do something like {Array.isArray(offers) && <SearchResult searchedOffers={offers} />}.
Often when doing something async like this, you might elect to actually use a ternary operator to show a loading indicator while the fetch is happening:
return (
<Aux>
<div className={classes.container}>
<div className={classes.contentSection}>
{offers ? <SearchResult searchedOffers={offers} /> : "Loading..."}
</div>
</div>
</Aux>
)

Return multiple Components depending on array prop

Trying to return multiple action buttons from a ActionButtons component:
export default class ActionButtons extends Component {
render() {
console.log(this.props.actions)
return(
this.props.actions.map((field, i) => {
<div key={field.href}>
<DefaultButton
key={field.href}
text={field.label}
href={field.href}
/>
</div>
})
)
}
}
Calling it from another component with the following code:
const actions = [
{"label": "Go Back", "href":"www.google.com"}
];
<ActionButtons actions={actions} />
On the ActionButtons component if i return just one single button without the map then it works. What am i missing ?
You need to explicitly return your jsx from inside map
//inside render
return this.props.actions.map((field, i) => {
return (
<div key={field.href}>
<DefaultButton
key={field.href}
text={field.label}
href={field.href}
/>
</div>
)
}
When using a jsx block () (the example above returns an array which is also valid) you also need to declare your operations inside curly brackets.
export default class ActionButtons extends Component {
render() {
console.log(this.props.actions)
return (
<>
{
this.props.actions.map((field, i) => {
return (
<div key={field.href}>
<DefaultButton
key={field.href}
text={field.label}
href={field.href}
/>
</div>
)
})
}
</>
)
}
}

How to pass a variable number of props to a JSX tag in React

I am trying to create a website which has many "showcase" pages. On each of them i have a variable number of images (not always the same amount) and im using props to pass images to each page in their respective JSX tags as shown below. But i don't want to have: const {title, image1, image2, image3, etc} = props
Is there a way that i can use a single image prop and in my JSX tag use it as many times for any number of images?
The code below is what i would ike to achieve but of course it doesn't work.
Hockey.js
class Sport extends Component {
render() {
return (
<div>
<Pages title="Hockey" image={hockey1, hockey2, hockey3}/>
<Pages title="Football" image={foot1, foot2}/>
</div>
)
}
}
Pages.js
const Pages = props => {
const { title, image } = props
return ( <div></div>)
}
You can use an array to pass a list of images;
class Sport extends Component {
render() {
return (
<div>
<Pages title="Hockey" images={[hockey1, hockey2, hockey3]} />
<Pages title="Football" images={[foot1, foot2]} />
</div>
)
}
}
Then use in your component:
const Pages = props => {
const {title, images} = props;
return (<div class="pages">
{images.map((image, index) => (
<div class="page" key={index}>
<img src={image} />
</div>
)}
</div>);
}

Resources