I want to pass a string from a child component to its parent, which in turn will update the data passed to the child component. For example, when a link is clicked, change planet data to reflect new planet.
There are three components.
Parent
Middle-man
Child
Hierarchy
Home => Navbar => ExpandedMenu
Parent Component
I have mapped each planet to an object called planetData from the json data. When a link is clicked, I want it to update the contents assigned to the planet passed.
import React, { useState, useEffect } from "react";
import Navbar from "../../shared/components/Navbar/Navbar";
import MobileView from "../../home/components/MobileView/MobileView";
import TabletView from "../../home/components/TabletView/TabletView";
import DesktopView from "../../home/components/DesktopView/DesktopView";
import Page from "../../shared/interface/Page/Page";
import data from "../../db/data.json";
function Home() {
// responds to events, changes active planet to represent current
const [activePlanet, setActivePlanet] = useState(data[0]);
// map each planet in json data to objects
const planetData = {
mercury: data[0],
venus: data[1],
earth: data[2],
mars: data[3],
jupiter: data[4],
saturn: data[5],
uranus: data[6],
neptune: data[7],
};
// render complete page to document
return (
<Page>
<Navbar />
{/* Conditional rendering based on screen width */}
{isDesktop && <DesktopView content={activePlanet} />}
{isTablet && <TabletView content={activePlanet} />}
{isMobile && <MobileView content={activePlanet} />}
</Page>
);
}
export default Home;
Middle Component
Component is purely a middle man.
import React, { useState } from "react";
import HamburgerMenu from "../../../assets/icon-hamburger.svg";
import ExpandedMenu from "../ExpandedMenu/ExpandedMenu";
import NavMenu from "../NavMenu/NavMenu";
function Navbar() {
const [isVisible, setIsVisible] = useState(true);
return (
<nav>
<NavMenu />
<ExpandedMenu isToggled={isVisible} />
</nav>
);
}
export default Navbar;
Child Component
When a link is clicked, it updates the state to a string representing the planet.
import React, { useState } from "react";
import NavLink from "../NavLink/NavLink";
import Arrow from "../../../assets/icon-chevron.svg";
function ExpandedMenu({ isToggled }) {
const [planet, setPlanet] = useState("Mercury");
return (
<div>
{/* Mercury */}
<div>
<div>
<NavLink
clickAction={() => {
setPlanet("Mercury");
}}
>
Mercury
</NavLink>
</div>
<img src={Arrow} alt="arrow" />
</div>
{/* Venus */}
<div>
<div>
<NavLink
clickAction={() => {
setPlanet("Venus");
}}
>
Venus
</NavLink>
</div>
<img src={Arrow} alt="arrow" />
</div>
</div>
);
}
export default ExpandedMenu;
Related
i am trying to setting up darkMode button, but i got this error ... Uncaught Error: Objects are not valid as a React child (found: object with keys {background, color}). If you meant to render a collection of children, use an array instead.
here is my data..................
my Contextjs component......
import { createContext, useReducer } from "react";
export const themeContext = createContext();
const initialState = {darkMode : true};
const themeReducer = (state, action) =>{
switch(action.type){
case'toggle':
return {darkMode : !state.darkMode};
default:
return state;
}
};
export const ThemeProvider = (props)=>{
const [state, dispatch] = useReducer (themeReducer, initialState);
return(
<themeContext.Provider value={{state, dispatch}}>
{props.children}
</themeContext.Provider>
);
};
my AppJs component.......
import Navbar from "./Components/Navbar/Navbar";
import "./App.css"
import { Intro } from "./Components/Intro/Intro";
import Services from "./Components/Services/Services";
import Experience from "./Components/Experience/Experience"
import Works from "./Components/Works/Works"
import Portfolio from "./Components/Portfolio/Portfolio"
import Testimonials from './Components/Testtimonials/Testimonials'
import Contact from "./Components/Contact/Contact"
import Footer from "./Components/Footer/Footer"
import {themeContext} from './Context'
import { useContext } from "react";
function App() {
const theme = useContext(themeContext);
const darkMode = theme.state.darkMode;
return (
<div className="App">
style={{
background : darkMode? 'black' : '',
color : darkMode? 'white' : ''
}}
<Navbar />
<Intro />
<Services />
<Experience />
<Works />
<Portfolio/>
<Testimonials/>
<Contact />
<Footer />
</div>
);
}
export default App;
and here is IndexJs..........
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {ThemeProvider} from './Context';
ReactDOM.render(
<ThemeProvider>
<App />
</ThemeProvider>,
document.getElementById('root')
);
and error is:- Uncaught Error: Objects are not valid as a React child (found: object with keys {background, color}). If you meant to render a collection of children, use an array instead.
You have to pass style prop to component.
<div
className="App"
style={{
background: darkMode ? "black" : "",
color: darkMode ? "white" : "",
}}
>
I would rather prefer, create 2 classes dark and light and use like this:
<div className={`App ${darkMode ? "dark" : "light"}`}>
It's trying to render your style object as a child of the div, update it so it is an attribute of the div.
<div
className="App"
style={{
background: darkMode ? "black" : "",
color: darkMode ? "white" : "",
}}
>
<Navbar />
<Intro />
<Services />
<Experience />
<Works />
<Portfolio />
<Testimonials />
<Contact />
<Footer />
</div>
I'm currently building a React application with the following workflow:
List of application categories, the user will select one
Once a user has selected an application category, they will select a water type from a list
A list of products will then be displayed depending on the category and type selected.
They can then select a product to see the information i.e. product charts, images etc.
The problem:
Once a user selects a product, if they click the back button, the category and type props are lost.
Solution required:
I need to be able to maintain these props/state at all times, allowing them to be updated if the user goes back and changes category/type
I've included my code for reference below.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById("root")
);
App.js
import React from "react";
import { Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import WaterType from "./components/WaterType";
import Products from "./components/Products";
import Product from "./components/Product";
import "./App.css";
function App() {
return (
<div className="App">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/waterType" component={WaterType} />
<Route path="/products/:productName" component={Product} />
<Route path="/products" component={Products} />
</Switch>
</div>
);
}
export default App;
Home.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
import CategoryData from "./data/CategoryData";
class Home extends Component {
render() {
return (
<>
<h1>Categories</h1>
<ul>
{CategoryData.map((cat, i) => (
<li key={i}>
<Link
to={{
pathname: "/waterType",
name: cat.name,
}}
>
<img src={cat.imageURL} alt={cat.name} />
{cat.name}
</Link>
</li>
))}
</ul>
</>
);
}
}
export default Home;
WaterType.js
import React from "react";
import { Link } from "react-router-dom";
import WaterTypeData from "./data/WaterTypeData";
const WaterType = ({ location }) => {
const categorySelected = location.name;
return (
<>
<h1>Water Types</h1>
<p>Current category: {categorySelected}</p>
<ul>
{WaterTypeData.map((type, i) => (
<li key={i}>
<Link
to={{
pathname: "/products",
categorySelected: categorySelected,
waterType: type.name,
}}
>
{type.name} - {type.description}
</Link>
</li>
))}
</ul>
</>
);
};
export default WaterType;
Products.js
import React from "react";
import { Link } from "react-router-dom";
import ProductData from "./data/ProductData";
const Products = ({ location }) => {
const categorySelected = location.categorySelected;
const waterType = location.waterType;
const ProductsResult = ProductData.filter(x => x.categories.includes(categorySelected) && x.waterTypes.includes(waterType));
return (
<>
<h1>Products</h1>
<p>Current category: {categorySelected && categorySelected}</p>
<p>Water Type: {waterType && waterType}</p>
<div className="products">
<ul>
{ProductsResult.map((item, i) => (
<li key={i}>
<Link
to={{
pathname: '/products/' + item.slug,
name: item.name,
}}
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
</>
);
};
export default Products;
Product.js
import React from "react";
const Product = ({ location }) => {
const productName = location.name;
return (
<>
<h1>{productName}</h1>
</>
);
};
export default Product;
The easiest solution that I can think of is to keep your selected choices (category and water type) in a top level context.
Something like this:
// ChoicesProvider.js
import React, { createContext, useState } from "react";
export const ChoicesContext = createContext(null);
export const ChoicesProvider = ({ children }) => {
const [choices, setChoices] = useState({
category: null,
waterType: null,
});
return (
<ChoicesContext.Provider value={{ choices, setChoices }}>
{children}
</ChoicesContext.Provider>
);
};
…and then in your entry point:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
import { ChoicesProvider } from "./context/ChoicesProvider";
ReactDOM.render(
<React.StrictMode>
<ChoicesProvider>
<Router>
<App />
</Router>
</ChoicesProvider>
</React.StrictMode>,
document.getElementById("root")
);
…and then each time you pick a category / waterType save the selected state in a context using setChoices defined in context. For example:
// Home.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
import CategoryData from "./data/CategoryData";
import { ChoicesContext } from "../../context/ChoicesContext";
const Home = () => {
const { choices, setChoices } = useContext(ChoicesContext);
return (
<>
<h1>Categories</h1>
<ul>
{CategoryData.map((cat, i) => (
<li key={i}>
<Link
onClick={() => setChoices({ ...choices, category: cat.name })}
to={{
pathname: "/waterType",
name: cat.name,
}}
>
<img src={cat.imageURL} alt={cat.name} />
{cat.name}
</Link>
</li>
))}
</ul>
</>
);
};
export default Home;
Hopefully that gives you an idea.
Have a great day 👋
First of all, I am not sure if your Router setup is necessary. React is great for single page applications (SPA), this means, you don't neet new page for every single functionality. You can very comfortably build your application on single Route, changing just the components you currently need.
So the first possible solution is to build the application on single page, using three simple states
[category, setCategory] = useState(null)
[waterType, setWatertype] = useState(null)
[product, setProduct] = useState(null)
Based on this you can simply show or hide the select options
<div id="selections">
{!category && (<Categories />)}
{(category && !watertype) && (<WaterTypes />)}
{category && watertype) && (<Products />)}
</div>
category && <WaterTypes /> means, WaterTypes will be displayed only if category is true.
This if course requires your Link to be replaced with something like
<button type="button" onClick={() => setCategory(cat.name)}>{cat.name}</button>
With this approach you can then handle back and forward button to manipulate your states and URL, so the user has access to desired category with hyperlink like www.youreshop.com?category=first&watertype=second
Another approach is to add context to your app, what would allow you to share states between individual components.
I highly recommend Redux (https://redux.js.org/) for application state managemt.
In general it's a good idea to include Redux to modern React applications, and in your case it seems to be exactly what you're looking for.
Hope this helps.
Cheers
Is there a cleaner way to do a condition in a react component? Say I have something like this:
<Track>
<ProductLink>
<Image ... />
<Typography ...>{...}</Typography>
<Typography ...>{...}</Typography>
</ProductLink>
</Track>
And with conditions, it would be like this:
{condition ? <Track><ProductLink> : <> }
<Image ... />
<Typography ...>{...}</Typography>
<Typography ...>{...}</Typography>
{condition ? </ProductLink></Track> : </> }
Is there another way of doing the condition?
If condition is met, have the Track and ProductLink component, if not, just display the Fragment component.
I'd recommending hoisting the Track and ProductLink into a reusable component -- that way you'll simplify it to one condition.
For example:
import React from "react";
import PropTypes from "prop-types";
import Track from "./path/to/Track";
import ProductLink from "./path/to/ProductLink";
const TrackProduct = ({ children, isTracked }) => (
isTracked
? <Track>
<ProductLink>
{children}
</ProductLink>
</Track>
: children
);
TrackProduct.proptTypes = {
children: PropTypes.node.isRequired,
isTracked: PropTypes.bool.isRequired
};
export default TrackProduct;
Then import the above reusable component and control it via state:
import React from "react";
import TrackProduct from "./path/to/TrackProduct";
import Image from "./path/to/Image";
import Typography from "./path/to/Typography";
import Button from "./path/to/Button";
class Example extends Component {
state = { isTracked: false };
toggleTrack = () => this.setState(prevState => ({ track: !prevState.isTracked }))
render = () => (
<TrackProduct isTracked={this.state.isTracked}>
<Image ... />
<Typography ...>{...}</Typography>
<Typography ...>{...}</Typography>
<Button onClick={this.toggleTrack}>
{!this.state.isTracked ? "Track" : "Untrack"} Product
</Button>
</TrackProduct>
)
};
export default Example;
If the above doesn't quite work and/or is too repetitive, then you can use a render prop that passes some of its own state and class fields to a child function to be controlled (I prefer this method over wrapper HOCs, since they're easier to test against).
For example:
import React, { Component } from "react";
import PropTypes from "prop-types";
import Track from "./path/to/Track";
import ProductLink from "./path/to/ProductLink";
class TrackProduct extends Component {
state = { isTracked: false };
toggleTrack = () => this.setState(prevState => ({ track: !prevState.isTracked }))
render = () => {
const renderedChildren = this.props.children({
isTracked: this.state.isTracked,
toggleTrack: this.toggleTrack
});
return (
isTracked
? <Track>
<ProductLink>
{renderedChildren}
</ProductLink>
</Track>
: renderedChildren
)
};
};
TrackProduct.proptTypes = {
children: PropTypes.func.isRequired,
};
export default TrackProduct;
Then import the above reusable render prop component and control it via TrackProduct's passed down props (isTracked and toggleTrack):
import React from "react";
import TrackProduct from "./path/to/TrackProduct";
import Image from "./path/to/Image";
import Typography from "./path/to/Typography";
import Button from "./path/to/Button";
const Example = () => (
<TrackProduct>
{({ isTracked, toggleTrack }) => (
<>
<Image ... />
<Typography ...>{...}</Typography>
<Typography ...>{...}</Typography>
<Button onClick={toggleTrack}>
{!isTracked ? "Track" : "Untrack"} Product
</Button>
</>
)}
</TrackProduct>
);
export default Example;
More info about render props can be found in this Phoenix ReactJS conference talk.
I have the following code: Link to Sandbox
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
import "./styles.css";
function DropDown({ close }) {
return (
<ul>
<li>
<Link to="/" onClick={close}>
Page 1
</Link>
</li>
<li>
<Link to="/page2" onClick={close}>
Page 2
</Link>
</li>
<li>
<p>Dont hide dropdown when clicking me!</p>
</li>
</ul>
);
}
function Header() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
{isOpen && <DropDown close={() => setIsOpen(false)} />}
</div>
);
}
function Page1() {
return <h1>Page 1</h1>;
}
function Page2() {
return <h1>Page 2</h1>;
}
function App() {
return (
<div className="App">
<BrowserRouter>
<Header />
<Switch>
<Route exact path="/" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
</BrowserRouter>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
How can I implement this without always passing an close callback down to every link in my dropdown?
I am trying to implement this with a hook which listens on mousedown and checks if it is inside the dropdown and a click on type A but the problem is that then it closes the dropdown before the react redirect to the route.
The click outside I already have covered with a useClickAway hook but I would kind of need a useClickInsideOnLink hook as well.
I think you have (at least) two options:
move Header into the pages (due to new pages are rendered, the header is re-initialized). Forked your CodeSandbox here
let the DropDown handle it's state on it's own, and include the toggle button to the dropdown. See CodeSandbox here
Another possible way (I don't recommend it as it adds unnecessary complexity to a simple problem) (added by the comments of the asker):
Actually, a component updates, when props are changed or the state
changes. To quote react docs: If you want to “reset” some state when
a prop changes, consider either making a component fully controlled
or fully uncontrolled with a key instead. Fully uncontrolled would be
option 2 then.
To trigger the change of props, you could connect the Header withRouter.
This way the Header component get's notified when the location (and
other routing-props) change. Based on that, you could apply updates to the isOpen
state via the lifecycleMethod
componentDidUpdate
and compare the previous location.pathname with the current one and set the isOpen back to false if the pathname changed.
Actually I do not recommend that. But here you go:
CodeSandbox
You could create an internal component inside your DropDown component and use the close function by default, also passing the other props via rest parameter. Something like this:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link } from "react-router-dom";
import "./styles.css";
function DropDown({ close }) {
const MyLink = ({...rest}) => {
return (
<Link {...rest} onClick={close}>
Page 1
</Link>
)
}
return (
<ul>
<li>
<MyLink to="/">
Page 1
</MyLink>
</li>
<li>
<MyLink to="/page2">
Page 2
</MyLink>
</li>
<li>
<p>Dont hide dropdown when clicking me!</p>
</li>
</ul>
);
}
function Header() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
{isOpen && <DropDown close={() => setIsOpen(false)} />}
</div>
);
}
function Page1() {
return <h1>Page 1</h1>;
}
function Page2() {
return <h1>Page 2</h1>;
}
function App() {
return (
<div className="App">
<BrowserRouter>
<Header />
<Switch>
<Route exact path="/" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
</BrowserRouter>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Probably it's too late but if it can't help someone :
You can destrcuture pathname from useLocation() hook, and on every pathname change
you can set isOpen to false in useEffect.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch, Link, useLocation } from "react-router-dom";
import "./styles.css";
function DropDown({ close }) {
const MyLink = ({...rest}) => {
return (
<Link {...rest}>
Page 1
</Link>
)
}
return (
<ul>
<li>
<MyLink to="/">
Page 1
</MyLink>
</li>
<li>
<MyLink to="/page2">
Page 2
</MyLink>
</li>
<li>
<p>Dont hide dropdown when clicking me!</p>
</li>
</ul>
);
}
function Header() {
const [isOpen, setIsOpen] = useState(false);
const { pathname } = useLocation();
useEffect(() =>{
setIsOpen(false);
}, [pathname])
return (
<div>
<button onClick={() => setIsOpen(prev => !prev)}>Toggle DropDown</button>
{isOpen && <DropDown close={() => setIsOpen(false)} />}
</div>
);
}
function Page1() {
return <h1>Page 1</h1>;
}
function Page2() {
return <h1>Page 2</h1>;
}
function App() {
return (
<div className="App">
<BrowserRouter>
<Header />
<Switch>
<Route exact path="/" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
</BrowserRouter>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Try to subscribe on current location like this:
const location = useLocation();
useEffect(() => {
closeHeader();
}, [location]);
This one is good I guess ;3
I have a problem with passing context to route. I get an error when i click a link that goes to my component where context was passed from App component. Below is that component with App (only one import just to show where Context is coming from):
App.js
import { Context } from './Context';
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
cryptolist: []
}
}
componentDidMount = () => {
fetch('https://api.coinmarketcap.com/v2/ticker/?structure=array')
.then(response => response.json())
.then(json => this.setState({
cryptolist: json.data
}))
}
render() {
return (
<div>
<Menu />
<Context.Provider value={this.state}>
<Userlist />
</Context.Provider>
</div>
)
}
}
export default App;
Userlist.js ( should be cryptolist or something )
import { Context } from '.././Context'
export default class Userlist extends Component {
render() {
return (
<main>
<Context.Consumer>
{(context) => context.cryptolist.map(el => {
return (
<div>
<h2>{el.name}</h2>
<h5>{el.symbol}</h5>
<h3>{el.quotes.USD.price}</h3>
</div>
)
})}
</Context.Consumer>
</main>
)
}
}
Context.js
import React from 'react';
export const Context = React.createContext();
Everything works just fine here untill i wanted to make a menu that links to this component.
import React from "react";
import { slide as Slider } from 'react-burger-menu';
import { BrowserRouter as Router, Route, Link, Switch} from "react-router-dom";
import Main from './main';
import Userlist from './userlist';
export default class Menu extends React.Component {
render () {
return (
<Router>
<div className="bg-navy w-100 h-100">
<Slider width={ 180 } isOpen={ false }>
<Link className="menu-item" to="/main">Home</Link>
<Link className="menu-item" to="/crypto">About</Link>
</Slider>
<Switch>
<Route path="/main" component={Main} />
<Route path="/crypto" component={Userlist} />
</Switch>
</div>
</Router>
);
}
}
When i click a link to component Userlist i get an error thats cryptolist is not defined. I get it that Userlist can't see a context after clicking link to it. How to pass it correctly?
You are using the routes in the Menu component. Is this really you want? Though, I don't know how this slide thingy works. Maybe this is the way you want to go. I think your problem occurs because your Menu component is not wrapped by the provider. Try like this:
<Context.Provider value={this.state}>
<Menu />
<Userlist />
</Context.Provider
Your Menu component will call Userlist but as it is out the Provider the context doesn’t exist!
Replace Userlist in Context.Provider by Menu and all will be fine.