Learning react and using semantic-ui-react Menu to build a SideBarMenu , and have below Questions
How does clicking on a Menu.Item trigger parent component (SBMenu) render ?
like render on component is triggered either due to change in state or props , but what is changing for SBMenu ?
Why is SBMenu render function called twice ?
Rendering .... future
Rendering .... future
SidebarMenu
import React from 'react';
import { Sidebar, Menu, Segment, Icon } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import SBMenu from './SBMenu'
import './SidebarMenu.css';
import SBRoutes from './SBRoutes'
const menuItems = [
{ icon: 'dashboard', label: 'Future ITEM', name: 'future', url: '/future' },
{ icon: 'dashboard', label: 'Future ITEM1', name: 'future', url: '/future1' }
];
class SidebarMenu extends React.Component {
constructor(props) {
super(props);
this.handleItemClick = this.handleItemClick.bind(this);
}
handleItemClick () {
console.log('item clicked')
}
/*shouldComponentUpdate() {
return true;
}*/
render() {
console.log("SidebarMenu called....")
return (
<Sidebar.Pushable as={Segment} className="SidebarMenu">
<Sidebar
as={Menu}
borderless
animation="push"
icon="labeled"
inverted
onHide={this.handleSidebarHide}
vertical
visible={true}
width="thin"
>
<SBMenu menuItems={menuItems} />
</Sidebar>
<Sidebar.Pusher>
<div className="container">
<SBRoutes />
</div>
</Sidebar.Pusher>
</Sidebar.Pushable>
);
}
}
export default SidebarMenu;
SBMenu
class SBMenu extends React.Component {
constructor(props) {
super(props);
}
render() {
const {location,menuItems} = this.props;
console.log("Rendering .... " + location.pathname)
return (menuItems.map((item, index) => (
<Menu.Item
name={item.name}
as={Link}
to={item.url}
active={location.pathname === item.url}
data-menu={item.label}
key={`menu_item_${index}`}
>
<Icon name={item.icon} />
{item.label}
</Menu.Item>
))
);
}
}
export default withRouter(SBMenu);
You can change child state from parent using lifting state up. You can simply pass a method to the SBMenu and work with it.
SBMenu render twice because you use the same name in menuItems.
It should be working fine now if you change menuItems name.
Related
On a React page I have:
<a
data-tab="settings"
onClick={() =>
this.setState({ active_tab: "settings" })
}
>
<i style={{ backgroundImage: "url(/icons/settings.svg)" }} />
<span>To settings</span>
</a>
How can I refactor this to a Component? How can I update the parent component (i.e., the React page) its local state from a Component?
1. With classes: based on #tom203 his answer below
Component file:
import React, { Component } from "react";
class MyButton extends Component {
constructor(props) {
super(props);
this.state = {
tab: props.tab,
icon: props.icon,
label: props.label,
};
}
render() {
return (
<div>
<button
onClick={ () => this.props.updateActiveTab(this.state.tab) }
data-tab={ this.state.tab }
>
<i style={{ backgroundImage: `url(${this.state.icon})` }} />
<span> { this.state.label } </span>
</button>
</div>
)
}
};
export default MyButton;
React page (parent Component):
import MyButton from "../components/MyButton/";
class MyReactPage extends Component {
updateActiveTab = value => {
this.setState({ active_tab: value });
}
render() {
return (
...
<MyButton
updateActiveTab={this.updateActiveTab}
tab="settings"
icon="/icons/settings.svg"
label="To settings"
/>
...
2. With React hooks
Component file:
import React, { useState } from "react";
const MyButton = (tab, icon, label) => {
const [active_tab, setActive_tab] = useState("data");
return (
<button
data-tab={ tab }
onClick={ () => setActive_tab({ tab }) }
>
<i style={{ backgroundImage: `url(${icon})` }} />
<span> { label } </span>
</button>
);
};
export default MyButton;
On React page (parent Component):
import MyButton from "../components/MyButton/";
class MyReactPage extends Component {
render() {
return (
...
MyButton({
tab: "settings",
icon: "/icons/settings.svg",
label: "To settings",
})
...
The hook setup generates two errors:
Invalid hook call. Hooks can only be called inside of the body of a
function component.
'active_tab' is declared but its value is never read.
You can't use this type of states in a function. You need a React component class or use generally React hooks.
So replace
const MyButton = (props) => {
by
class MyButton extends Component {
constructor(props) {
super(props);
}
and use a
render() {return (<div><a onClick={this.props.updateActiveTab(tab)} ...>XXX</a></div>)}
Call component:
<MyButton updateActiveTab={this.updateActiveTab} />
Main class:
...
updateActiveTab = value => {
this.setState({active_tab: {value}});
}
...
With Functional Components you have to use React Hooks.
Here's a link with an overview: https://reactjs.org/docs/hooks-intro.html
In your case it looks like you just need to use the useState hook to keep track of and change your active_tab state.
Here's How I would do it:
import React from "react";
const MyButton = ({tab, icon, label}) => {
const [tab, setTab] = useState(tab)
return (
<div className="tab-button">
<a
data-tab={tab}
// next line doesn't work...
onClick={() => setTab(tab)}
>
<i style={{ backgroundImage: `url(${icon})` }} />
<span> {label} </span>
</a>
</div>
);
};
The useState calls you see return the current state and a function to change the state.
For example:
const [tab, setTab] = useState(tab)
Here tab is the current state and setTab is a function that takes an argument, in this case a tab, it would then change the current state to whatever was passed in.
Hope that helps some. Let me know :)
I want to pass brand.title to the child component - BrandDetail
This is my try and is not working, it simply renders the child component within the parent component and I want it to be rendered solely on the child component.
Parent component:
class BrandsList extends React.Component {
state = {
brands: [],
};
fetchBrands = () => {
axios.get('http://127.0.0.1:8000/api/brands/').then((res) => {
this.setState({
brands: res.data,
});
});
};
componentDidMount() {
this.fetchBrands();
}
render() {
return (
<div style={{ margin: 22 }}>
{this.state.brands.map((brand) => (
<div key={brand.id}>
<Link to={`/brands/${brand.id}`}>{brand.title}</Link>
<BrandDetail brandName={brand.title} />
</div>
))}
</div>
);
}
}
export default BrandsList;
Child component:
import React, { useEffect, useState } from 'react';
import MyLayout from '../MyLayout/MyLayout';
class BrandDetail extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<MyLayout>
<div>Yes this is the detail page of {this.props.brandName}</div>
</MyLayout>
);
}
}
export default BrandDetail;
UPDATE:
This is the answer I was looking for.
{ this.state.brands.map(brand =>
<div key={brand.id}>
<Link to={{
pathname: `/brands/${brand.title}`,
state: `${brand.title}`,
}}>{brand.title}
</Link>
</div>
)}
And child component:
<div>Yes this is the detail page of {props.location.state}</div>
So, I try to understand how can I make right redirection in my app with event clicks? I put the react-router-dom redirect logic into the button event handler, but it does not work.
What is I'm making wrong?
import React, { Component, Fragment } from 'react';
import Preloader from '../Preloader/Preloader'
import preloaderRunner from '../../Modules/PreloaderRunner'
import { Redirect } from 'react-router-dom';
import axios from 'axios';
class LoginPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
handleClick = () => {
console.log('Button is cliked!');
return <Redirect to="/employers" />
}
render() {
return (
<Fragment>
<Preloader/>
<h1>This is the Auth Page!</h1>
{this.state.navigate === true
? <div>
<div>You already loggined!</div>
<button onClick={this.handleClick}>Go to the Employers List!</button>
</div>
: <div>
<form>
// some code...
</form>
</div>}
</Fragment>
)
}
}
export default LoginPage;
Things returned by a click handler will not be rendered by your component. You have to introduce a new state property that you can set and then render the <Redirect> component when that property contains a path to redirect to:
class LoginPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false,
referrer: null,
};
}
handleClick = () => {
console.log('Button is cliked!');
this.setState({referrer: '/employers'});
}
render() {
const {referrer} = this.state;
if (referrer) return <Redirect to={referrer} />;
// ...
}
}
Alternatively instead of rendering your own button with a click handler you could render a <Link> component as suggested by #alowsarwar that will do the redirect for you when clicked.
I believe on click you want to take the user to '/employers' . Then you need to use Link from the react-router-com. Ideally in React events like 'handleClick' should change the state not return a JSX (this is the wrong approach)
import React, { Component, Fragment } from 'react';
import Preloader from '../Preloader/Preloader'
import preloaderRunner from '../../Modules/PreloaderRunner'
import { Redirect, Link } from 'react-router-dom';
import axios from 'axios';
class LoginPage extends Component {
constructor(props) {
super(props);
this.state = {
navigate: false
}
}
handleClick = () => {
this.setState({ navigate: true});
}
render() {
return (
<Fragment>
<Preloader/>
<h1>This is the Auth Page!</h1>
{this.state.navigate === true
? <div>
<div onClick="this.handleClick">If you want to enable link on some event (Sample test case fyr)</div>
{this.state.navigate ? <Link to='/employers'/> : null}
</div>
: <div>
<form>
// some code...
</form>
</div>}
</Fragment>
)
}
}
export default LoginPage;
could anyone tell me why is that won't work? Proper data is displaying in the console (console.log(this.state);), but it won't be transfered to MainContainer.
Same data initialized in the constructor>state>users working without issues. Where's the problem?
App
import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Header from './components/header/Header';
import MainContainer from './containers/main-container/MainContainer';
class App extends Component {
constructor(props) {
super(props);
this.state = {
users: []
}
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(users => {
let u = users.map((user) => {
return {id: user.id, name: user.name, email: user.email}
})
return u;
})
.then(u => {
this.setState({users: u});
console.log(this.state);
});
}
render() {
return (
<div className="App">
<Header/>
<MainContainer users={this.state.users}/>
</div>
)
}
}
export default App;
MainContainer
import React from 'react';
import ActionBar from '../../components/action-bar/ActionBar'
import ListHeader from '../../components/list-header/ListHeader'
import ListItem from '../../components/list-item/ListItem'
import ListItemPlaceholder from '../../components/list-item-placeholder/ListItemPlaceholder'
class MainContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
users : props.users
}
}
render() {
const list = this.state.users.map(
(user) =>
{
const liStyle = {
'background-color': user % 2 == 0 ? '#fbfcfc' : 'transparent',
};
return <ListItem key={user.id} style={liStyle} id={user.id} name={user.name} email={user.email}/>
}
);
return (
<div className={'main-container'}>
<ActionBar />
<ListHeader />
{list}
</div>
)
}
}
export default MainContainer;
.................................................................................................................
Best Regards!
crova
In your <MainContainer> component you store the users in its state in the constructor but you never alter it. You only need to use state when the component needs to alter it during its lifetime. But the users come from it's parent via the users prop which you never render. So just render that prop instead:
const MainContainer = props => (
<div className="main-container">
<ActionBar />
<ListHeader />
{props.users.map(({id, name, email}) => (
<ListItem
key={id}
style={{
backgroundColor: id % 2 === 0 ? '#fbfcfc' : 'transparent'
}}
id={id}
name={name}
email={email}
/>
))}
</div>
);
When the users change in the parent it will re-render and pass the new users array to the <MainContainer>.
Also note that if your component only renders props and has no own state it can be written as a stateless functional component.
I'd like to set PropTypes validation for my React component to warn if there are no children. But when I follow the PropTypes syntax recommended by the React docs, I get this error thrown:
prop type children is invalid; it must be a function, usually from the prop-types package, but received undefined.
When I step through in the Chrome debugger, I see that children is undefined when the validation is performed. But I specifically set children in defaultProps, so why is it undefined? I've tried several different values for defaultProps.children, including [] and ''. All of them produced the same error.
Note that my app works OK. It's only PropTypes validation that is failing.
Below is a simplified version of the problematic code. Note that I'm using static properties to define defaultProps (as recommended by Dan Abramaov). I'm using create-react-app so I have the Babel transform to enable the default class properties syntax.
import React, { Fragment } from 'react';
import PropTypes from 'prop-types'
import { TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap';
import classnames from 'classnames';
export class Tab extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
children: PropTypes.node.required,
}
static defaultProps = {
name: null,
children: ''
}
render () {
return (
<Fragment>
{this.props.children}
</Fragment>
)
}
}
export default class TabSet extends React.Component {
constructor(props) {
super(props);
this.state = {
activeTab: 0
};
}
// TODO: limit to only Tab child elements.
static propTypes = {
children: PropTypes.node.required,
}
static defaultProps = {
children: ''
}
toggle = (tab) => {
if (this.state.activeTab !== tab) {
this.setState({
activeTab: tab
});
}
}
render() {
return (
<Fragment>
<Nav tabs>
{this.props.children.map((tab,i) =>
<NavItem key={i} style={{cursor: 'pointer'}}>
<NavLink
className={classnames({ active: this.state.activeTab === i })}
onClick={() => { this.toggle(i); }}
>
{ tab.props.name }
</NavLink>
</NavItem>
)}
</Nav>
<TabContent activeTab={this.state.activeTab}>
{this.props.children.map((tab,i) =>
<TabPane key={i} tabId={i}>
{tab}
</TabPane>
)}
</TabContent>
</Fragment>
);
}
}
If it matters, here's a simplified example of how these components are used:
import React from 'react';
import TabSet, {Tab} from './TabSet';
import HomeTab from './HomeTab';
import FriendsTab from './FriendsTab';
import HangTimesTab from './HangTimesTab';
export default class MainContainer extends React.Component {
render() {
return (
<div>
<TabSet>
<Tab name="Home">
<HomeTab />
</Tab>
<Tab name="Hang Times">
<HangTimesTab />
</Tab>
<Tab name="Friends">
<FriendsTab />
</Tab>
</TabSet>
</div>
);
}
}
The error message is a little confusing, but it refers to the prop-type declaration for children (i.e. PropTypes.node.required), which is undefined because of a typo. It should be PropTypes.node.isRequired.