React - toggle display of one component from the onClick of another component - javascript

I'm trying to build my first React project, and am currently putting together a burger nav button, and a menu which appears when clicking the nav.
I've broken this into two components; Hamburger and MenuOverlay. The code for both is below.
Currently I have an onClick on Hamburger toggling a class on it, but how would I also toggle the menu from that click? It's hidden with display: none; by default. Probably a very basic question so apologies - still trying to get my head around React.
MenuOverlay
import React from 'react';
import { Link } from 'react-router';
const MenuOverlay = () => {
return (
<div className="menuOverlay">
<div className="innerMenu">
<p><Link to="/">Home</Link></p>
<p><Link to="/">About</Link></p>
<p><Link to="/">Contact</Link></p>
</div>
</div>
);
};
export default MenuOverlay;
Hamburger
import React, { Component } from 'react';
class Hamburger extends Component {
constructor(props) {
super(props);
this.state = { active: '' };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
var toggle = this.state.active === 'is-active' ? '' : 'is-active';
this.setState({active: toggle});
}
render() {
return (
<button className={`hamburger hamburger--emphatic fadein one ${this.state.active}`} onClick={this.handleClick} type="button">
<span className="homeMenuTextButton">Menu</span>
<span className="hamburger-box">
<span className="hamburger-inner"></span>
</span>
</button>
);
}
}
export default Hamburger;

In the most simplistic form you would have a container component that wraps around both of them and manages the state of the components.
<MenuContainer>
<Hamburger />
<MenuOverlay />
</MenuContainer>
And in <MenuContainer> you would have a state of active some quick pseudocode.
class MenuContainer extends React.Component {
constructor() {
super();
this.state = { active: false}
}
toggleMenu = () => {
// function that will toggle active/false
this.setState((prevState) => {
active: !prevState.active
});
}
render() {
return (
<div>
<Hamburger active={this.state.active} onClick={this.toggleMenu} />
<MenuOverlay active={this.state.active} />
</div>
)
}
}
so in hamburger you would just use the this.props.onClick to change the state of active and then in those corresponding components use the prop of this.props.active to determine what classes should be applied, etc.

Given that one element is not the parent of another element, you will have to pull up the variable keeping the toggle information up the chain of elements until it resides in one common place.
That is, keep the "active" state variable in an ancestor of the two elements and provide to the Hamburger a callback in the props that, when called, modifies the state of that ancestor component. At the same time, also pass the active state variable down to the MenuOverlay as a prop, and everything should work together.
See here for more information:
https://facebook.github.io/react/tutorial/tutorial.html#lifting-state-up
Specifically,
When you want to aggregate data from multiple children or to have two child components communicate with each other, move the state upwards so that it lives in the parent component. The parent can then pass the state back down to the children via props, so that the child components are always in sync with each other and with the parent.

Related

React: Inserting a child into an already-rendered parent

I'm new to React and having some difficulty trying to add a new child component to a component that has already been rendered.
I have an App component which initially contains a Main component (main menu).
I also have Popover components which I want to appear on top of Main when they are children of <App> (and hence siblings of <Main>).
These Popover components vary in number. Each <Popover> can contain buttons which launch another <Popover> over the top again. So the structure would be like
<App>
<Main></Main>
<Popover></Popover>
<Popover></Popover>
...
</App>
However, when the page first loads there are no Popover components open, and the<App> is rendered without any. Here is a stripped-down version of my code:
class App extends React.Component{
constructor(props){ super(props) }
render(){
return (
<div>{this.props.children}</div>
)
}
}
class Main extends React.Component{
constructor(props){ super(props) }
render(){
return (
//main menu stuff here
)
}
}
ReactDOM.render(<App><Main /></App>, root);
How can I add new <Popover>s to my <App> when the user clicks something? Before React I would simply do App.appendChild(Popover) kind of thing, but I'm quite lost here.
I should add that the elements the user will click to trigger an initial <Popover> are not contained within <Main>; they are outside of the <App>, as I am trying to slowly transition my existing page to using React. I think this could be part of my problem.
So basically in React, you have multiple ways of doing this, but to be more reliable you need to have data that represents the dynamic components you will render in your DOM. And to do this you need to create a state and a function that can add new information to your state. Then simply by sharing this function with your other components, you can trigger it from wherever you want, and this will update your state which will increase the amount of dynamic components you will render.
Take a look at this example
import { useState } from "react";
export default function App() {
const [popups, setPopups] = useState([]);
const addNewPopup = () => {
setPopups([...popups, { title: "I am a popup" }]);
};
return (
<div className="App">
<ChildComponent onClick={addNewPopup} />
{popups.map((p) => {
return <Popup title={p.title} />;
})}
</div>
);
}
function ChildComponent({ onClick }) {
return (
<div>
<p>I am a child component</p>
<button onClick={onClick}>Add new element</button>
</div>
);
}
function Popup({ title }) {
return <div>I am a popup with title = {title}</div>;
}

ReactJS component reloading unexpectedly on className change

TLDR;
Does adding a className to an exciting div reload the parent component in ReactJs? Added images near button to show console.log being called multiple times.
Here is the bug..
I'm building a simple 'order' app, which includes a sidebar.
I recreated the sidebar in a new project for a sanity check. Same issues. Here is the simple version.
class App extends Component {
constructor() {
super();
this.state = {
addList : [],
}
}
render() {
return (
<div className="App">
<Sidebar list = {this.state.addList}/>
</div>
);
}
}
export default App;
and in the sidebar component
class Sidebar extends Component {
constructor(props){
super(props);
this.state = {
active : false
}
}
toggleSidebar () {
if (this.state.active) {
this.setState({
active : false
})
} else {
this.setState({
active: true
})
}
}
render () {
return (
<div className={ 'sidebar ' + ((this.state.active) ? '' : 'hidden')}>
<button className='tab' onClick={(e)=>{this.toggleSidebar()}}>
TAB
</button>
<div className="itemList">
{console.log(this.props.list)}
</div>
</div>
)
}
}
export default Sidebar;
The sideBar class has a position: fixed and I move it out of the screen on a button click and adding a hidden className (.hidden { right: -x })
When an item gets selected in the parent app component, it gets added to its state (addItem).
The Sidebar component has that property passed into so when addItem get a new item, it displays it. It works just as predicted. I was able to add items and display them no problem.
I noticed the bug when I started adding number and doing price totals etc, because it seems the sidebar keep rendering I would find myself getting caught in infinite setState loops
Any solutions or advice?
Images for a those that are visual (clicking the tab and console displaying):
The answer is simple, I'm foolish. In fact I AM changing the state of sidebar causes a reload.
Therefor to get around this is having the parent component hold the values and pass them down as properties for the sidebar to just display. Therefor on reload the values don't change or re-add.

Passing down a className to a child component when called from another

I am trying to add a className to a child component of layouts/index.js in React with Gatsby. How can I pass the className in props to be used with the component when the onClick is registered in another component?
index.js
class Template extends React.Component {
constructor(props) {
super(props)
this.state = {
navIsVisible: false,
}
this.handleNavIsVisible = this.handleNavIsVisible.bind(this)
}
handleNavIsVisible() {
this.setState({
navIsVisible: !this.state.navIsVisible
})
}
render() {
const { children } = this.props
return (
<div>
<MenuButton navIsVisible={this.handleNavIsVisible}/>
<div className="page">
...
</div>
{/* Adding the class here seems to be the best option but does not activate onClick, yet does if adding to a div with Menu contained */}
<Menu className={`${this.state.navIsVisible ? 'nav-is-visible' : ''}`}/>
</div>
)
}
}
MenuButton.js to activate the class onClick
class MenuButton extends Component {
constructor(props) {
super(props)
this.menuClick = this.menuClick.bind(this)
}
menuClick(){
this.props.navIsVisible();
}
render () {
return (
<div className="sticky-menu-button">
<span onClick={this.menuClick}>Menu</span>
</div>
)
}
}
MenuButton.propTypes = {
navIsVisible: PropTypes.func,
}
Alternatively, within Menu.js but unsure how to pass the state change to this component?
The reason it works on a div and not on your Menu component is that when you pass it to a div it adds that class "directly" to the div HTML element but when you pass it to your component it just passes it as a prop. It really depends on what you do inside the render method of the Menu component and what you return from it. If you make sure to grab that prop and attach it to whatever it renders it will work just as it did on a div.
eg:
class Menu extends Component {
render () {
return (
<div className={this.props.className}>
<p> Menu Component </p>
</div>
)
}
}

Conditional rendering a button value

I'm creating a game in which the initial state of the cards is face down, upon clicking a single card, the card's icon is revealed. When two cards match, both cards are removed from the board.
The challenge I'm having is in my conditional rendering. When I click on one card, all cards reveal their icon, not just one per click. Here is the codesandbox
I'm trying to keep state as local as possible the the relevant components and also trying the do it the "React" way by not toggling classnames and directly manipulating the DOM. How can I fix this and is the btnMask class the right way to go? If I can get this, then matching id's on cards may be a quick implementation. Here is the Card class
class Card extends Component {
constructor(props) {
super(props);
this.state = {
hidden: true
};
this.revealIcon = this.revealIcon.bind(this);
}
revealIcon(e) {
console.log("clicked", e.target.id);
this.setState({ hidden: false });
}
render() {
const card = this.props.cardsEasy["cards"].map((card, index) => {
if (this.state.hidden) {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div style={btnMask}>{card}</div>
</button>
);
} else {
return (
<button
key={card + index}
style={btn}
onClick={this.revealIcon}
id={card}
>
<div>{card}</div>
</button>
);
}
});
return <div style={divContainer}>{card}</div>;
}
}
export default Card;
You problem is that you have a common state for all cards. This means you have Card Component in which you render all the cards. The component has the state of hidden and renders a bunch of cards. Those cards access all the same variable in the state to check if they should hide. Therefore if you change that variable, all of then show up. What I would suggest you to do is to create a component "CardCollection" which renders the card.
class CardCollection extends React.Component {
render() {
this.props.cardsEasy["cards"].map((item) => {
<Card {...item} />
})
}
}
Like this each CardComponent will have their own state. You will need to adjust your CardComponent a little bit.
Instead of using a class component / stateful component you could also use a statless component for the CardCollection, as the collection doesn't need any state and is only there to render the childrens.
const CardCollection = (props) => {
return this.props.cardsEasy["cards"].map((item) => {
<Card {...item} />
});
}

Require children of certain type in React Component

I am creating a custom React Component for a Dropdown menu. The structure I had in mind to use this component was something among the lines of
<Dropdown>
<DropdownTrigger> // The button that triggers the dropdown to open or close
open me
</DropdownTrigger>
<DropdownButton> // A button inside the dropdown
Option 1
</DropdownButton>
</Dropdown>
The way I wanted to implement this, is by having the Dropdown component check if there is a DropdownTrigger component in its children, and if there is, clone the given DropdownTrigger component using React.cloneElement and pass an onClick property, that calls a state update on the Dropdown component.
Small code snippet of Dropdown.js
import DropdownTrigger from './DropdownTrigger';
_toggleDropdown () {
this.setState({
isOpen: !this.state.isOpen
});
}
_renderTriggerButton () {
const triggerChild = this.props.children.find(child => child.type === DropdownTrigger);
return React.cloneElement(triggerChild, {
...triggerChild.props,
onClick: () => this._toggleDropdown()
});
}
Is this a correct approach and if so, what would be the cleanest/nicest possible way to validate the Dropdown component has a DropdownTrigger as child. As this means a developer always has to include a DropdownTrigger as child of the Dropdown component, I would like to have a nice way to tell developer they should pass a <TriggerButton> component as child of the dropdown.
I'm also open for suggestions about changes in the structure. Thanks!
Use React.cloneElement to pass additional properties to child components. In combination with instanceof you can do exactly what you want.
It could be something along the lines...
import React from 'react';
import DropdownTrigger from './DropdownTrigger';
class DropDown extends React.Component {
constructor(props) {
super(props);
}
...
render() {
return (
<div>
{React.Children.map(this.props.children, child => {
const additionalProps = {}
// add additional props if it's the DropdownTrigger
if (child instanceof DropdownTrigger) {
additionalProps.toggleDropDown = () => { } ...
}
return React.cloneElement(child, additionalProps);
})}
</div>
);
}
}
export default DropDown;

Categories