I have an application has been written with React + Redux and Antdesign. My application is a dashboard app. So I used the layout in Ant design https://ant.design/components/layout/
When I click on side menus, the active menu gets bold which is fine. But I need when I refresh the page, it checks and detect the route and bold related menu item.
I have a Sidebar component which is stateful. Inside it, in componentDidMount I call a function which will dispatch an action from mapDispatchToProps. The reducer changes the state. But in HTML codes, in defaultSelectedKeys, I can not set the number of active menus.
Sidebar.js component:
import React from 'react';
import { render } from 'react-dom';
import { connect } from 'react-redux'
import { Switch, BrowserRouter, Route, Link } from 'react-router-dom';
// antd
import { Layout, Breadcrumb, Menu, Icon } from 'antd';
const { Header, Content, Footer, Sider } = Layout;
// Helpers
import { Alert } from '../helpers/notifications';
// Components
import Home from '../components/Home';
// import Header from '../components/Header';
import NotFound from '../components/NotFound';
import PostsEditor from '../components/Posts/PostsEditor';
// Actions
import { setRouteActiveFlag } from '../actions/ui.action'
class Sidebar extends React.Component {
componentDidMount () {
const routes = {
'/' : 1,
'/posts' : 2,
'/logout' : 3
}
this.props.detectActiveRoute(setRouteActiveFlag({
routes:routes,
path:window.location.pathname
}))
}
render() {
const { selectedRoute } = this.props;
console.log(selectedRoute);
return (
<div>
<Layout>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
}}
breakpoint="lg"
collapsedWidth="0"
onBreakpoint={broken => {
console.log(broken);
}}
onCollapse={(collapsed, type) => {
console.log(collapsed, type);
}}
>
<div className="logo" >
Logo <br/><br/><br/>
</div>
<Menu theme="dark" mode="inline" style={{ lineHeight: '64px' }} defaultSelectedKeys={[selectedRoute.toString() || '1']}>
<Menu.Item key="1">
<Link to="/" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Home</span>
</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/posts" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Posts</span>
</Link>
</Menu.Item>
<Menu.Item key="3">
<a href="/logout" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Logout</span>
</a>
</Menu.Item>
</Menu>
</Sider>
<Layout style={{ marginLeft: 200 }}>
<Content style={{ margin: '24px 16px 0', overflow: 'initial'}}>
<Breadcrumb style={{ margin: '0 0 20px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/posts/:id?" component={PostsEditor} />
<Route component={NotFound}/>
</Switch>
<Alert stack={ { limit: 3 } } />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
</Layout>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
state: state,
props: ownProps,
selectedRoute:state.ui.selectedRoute || 1
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
detectActiveRoute: (obj) => dispatch(obj)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
ui.action.js
export const setRouteActiveFlag = (payload = 'global') => ({
type: actions.SET_ROUTE_ACTIVE_FLAG,
payload
});
ui.reducer.js
import { handleActions } from 'redux-actions';
import Immutable from 'seamless-immutable';
import * as actions from '../consts/action-types';
const initialState = Immutable({
requests: {},
selectedRoute:{}
});
export default handleActions({
[actions.SET_ROUTE_ACTIVE_FLAG]: (state, action) => {
if (action.payload.routes && action.payload.path && action.payload.routes[ action.payload.path ]) {
return state.set('selectedRoute', action.payload.routes[ action.payload.path ])
}else{
return state.set('selectedRoute', 1)
}
}
}, initialState);
Please help me find the best and simple practices.
There is no need to use redux, just use react-router to get current pathname and pass it to defaultSelectedKeys.
<Menu defaultSelectedKeys={[this.props.location.pathname]}>
...
.....
</Menu>
Look at this answer , if you don't know how to get pathname
The following answer assumes you are using hooks. I know that from your question you are not using hooks, but it could be useful for other people. This answer works not only when refreshing but also when pressing the back and forward buttons:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Layout, Menu } from 'antd'
const { Sider } = Layout
const items = [
{ key: '1', label: 'Invoices', path: '/admin/invoices' },
{ key: '2', label: 'Service Details', path: '/admin/service-details' },
{ key: '3', label: 'Service Contract Details', path: '/admin/service-contract-details' },
{ key: '4', label: 'Cost Centers', path: '/admin/cost-centers' },
{ key: '5', label: 'Clients', path: '/admin/clients' },
{ key: '6', label: 'Vendors', path: '/admin/vendors' }
]
const Sidebar = () => {
const location = useLocation()
const history = useHistory()
const [selectedKey, setSelectedKey] = useState(items.find(_item => location.pathname.startsWith(_item.path)).key)
const onClickMenu = (item) => {
const clicked = items.find(_item => _item.key === item.key)
history.push(clicked.path)
}
useEffect(() => {
setSelectedKey(items.find(_item => location.pathname.startsWith(_item.path)).key)
}, [location])
return (
<Sider style={{ backgroundColor: 'white' }}>
<h3 style={{ paddingLeft: '1rem', paddingTop: '1rem', fontSize: '1.25rem', fontWeight: 'bold', minHeight: 64, margin: 0 }}>
Costek
</h3>
<Menu selectedKeys={[selectedKey]} mode='inline' onClick={onClickMenu}>
{items.map((item) => (
<Menu.Item key={item.key}>{item.label}</Menu.Item>
))}
</Menu>
</Sider>
)
}
export default Sidebar
Your sidebar will look as follows:
You can add any css in your menu by conditioning and adding a class just in this way.
<MenuItem className={ (this.props.location.pathname==='/yourRoute')? 'active' : '' } >
</MenuItem>
In case if you get any kind of undefined error then you can use the 'withRouter' HOC
in this way.
In your component where you want to get that location prop, you will first import
import {withRouter} from 'react-router-dom';
then you can export it in this way.
export default withRouter(YourComponent);
Final code can look somewhat similar to this
import React, {Fragment, Component} from 'react';
import {withRouter, Link } from 'react-router-dom';
class Menu extends Component {
render(){
const {pathname} = this.props.location;
return (
<Fragment>
<div id="sidebar-menu" className="sidebar-menu">
<ul>
<li className={(pathname==='/dashboard' || pathname==='/')?'active':''}>
<Link to="/dashboard">Dashboard</Link>
</li>
<li className={(pathname==='/properties')?'active':''}>
<Link to="/properties">Properties</Link>
</li>
</ul>
</div>
</Fragment>
);
}
}
export default withRouter(Menu);
Related
Would anyone be able to give me help regarding React Navigation with Expo? The issue is with the drawer component where the 'Hamburger' icon isn't opening or closing the component when a user presses the icon. However, it does open/close on a default swipe gesture from React Navigation. Please see below for my code:
Router:
import React from 'react';
import { NavigationContainer, DrawerActions } from '#react-navigation/native';
import { createDrawerNavigator } from '#react-navigation/drawer';
import { IconButton } from 'react-native-paper'
import i18n from '../i18n/i18n';
//Theme
import Theme from '../theme/theme'
//Components
import Menu from '../components/Menu'
//Import Interfaces
import { RootDrawerParamList } from '../utils/typescript/type.d';
import { IProps } from '../utils/typescript/props.d';
//Import Screens
import Screen1 from '../screens/Screen1';
import Screen2 from '../screens/Screen2';
import SettingsScreen from '../screens/Settings';
const Drawer = createDrawerNavigator<RootDrawerParamList>();
export default class Router extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
}
render() {
return (
<NavigationContainer>
<Drawer.Navigator
initialRouteName='Screen1'
drawerContent={(props: any) => <Menu {...props} />}
screenOptions={({
swipeEnabled: true
})}>
<Drawer.Screen
name="Screen1"
component={Screen1}
initialParams={{ i18n: i18n, Theme: Theme }}
options={({route, navigation} : any) => ({
headerRight: () => (<IconButton icon="cog" size={24} color={Theme.colors.text} onPress={() => navigation.navigate('Settings')} />),
route: {route},
navigation: {navigation}
})}
/>
<Drawer.Screen
name="Settings"
component={SettingsScreen}
initialParams={{ i18n: i18n, Theme: Theme }}
options={({route, navigation} : any) => ({
headerTitle: i18n.t('settings', 'Settings'),
headerLeft: () => (<IconButton icon="arrow-left" color={Theme.colors.text} size={24} onPress={() => navigation.goBack()} />),
route: {route},
navigation: {navigation}
})}
/>
<Drawer.Screen
name="Screen2"
component={Screen2}
initialParams={{ i18n: i18n, Theme: Theme }}
options={({route, navigation} : any) => ({
route: {route},
navigation: {navigation}
})}
/>
</Drawer.Navigator>
</NavigationContainer>
);
}
}
Menu
import React from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { List, Title } from 'react-native-paper';
import { getDefaultHeaderHeight } from '#react-navigation/elements';
import { DrawerItem } from '#react-navigation/drawer';
import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context';
//Import Interfaces
import { IListItem } from '../utils/typescript/types.d';
import { IPropsMenu } from '../utils/typescript/props.d';
import { IStateMenu } from '../utils/typescript/state.d';
//A function is used to pass the header height, using hooks.
function withHeightHook(Component: any){
return function WrappedComponent(props: IPropsMenu) {
/*
Returns the frame of the nearest provider. This can be used as an alternative to the Dimensions module.
*/
const frame = useSafeAreaFrame();
/*
Returns the safe area insets of the nearest provider. This allows manipulating the inset values from JavaScript. Note that insets are not updated synchronously so it might cause a slight delay for example when rotating the screen.
*/
const insets = useSafeAreaInsets();
return <Component {...props} headerHeight={getDefaultHeaderHeight(frame, false, insets.top)} />
}
}
class Menu extends React.Component<IPropsMenu, IStateMenu> {
constructor(props: IPropsMenu) {
super(props);
this.state = {
menu: [
{
name: 'screen1.name',
fallbackName: 'Screen 1',
icon: 'dice-multiple-outline',
iconFocused: 'dice-multiple',
onPress: this.props.navigation.navigate.bind(this, 'screen1')
},
{
name: 'screen2.name',
fallbackName: 'Screen 2',
icon: 'drama-masks',
iconFocused: 'drama-masks',
onPress: this.props.navigation.navigate.bind(this, 'screen2')
}
]
}
}
renderItem = (item : IListItem) => {
const { i18n } = this.props.state.routes[0].params;
return (
<DrawerItem
label={ i18n.t(item.name, item.fallbackName) }
onPress={ item.onPress ? item.onPress: () => {} }
icon={ ({ focused, color, size }) => <List.Icon color={color} style={[styles.icon, {width: size, height: size }]} icon={(focused ? item.iconFocused : item.icon) || ''} /> }
/>
);
};
render() {
const { headerHeight } = this.props;
const { menu } = this.state;
const { Theme } = this.props.state.routes[0].params;
return (
<View>
<View style={{
backgroundColor: Theme.colors.primary,
height: headerHeight ?? 0,
}}>
<View style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
}}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 5,
marginLeft: 5
}}>
<Title style={{ color: Theme.colors.text, marginLeft: 5 }}>
Title
</Title>
</View>
</View>
</View>
<FlatList
data={menu}
keyExtractor={item => item.name}
renderItem={({item}) => this.renderItem(item)}
/>
</View>
);
};
}
export default withHeightHook(Menu);
const styles = StyleSheet.create({
icon: {
alignSelf: 'center',
margin: 0,
padding: 0,
height:20
},
logo: {
width: 24,
height: 24,
marginHorizontal: 8,
alignSelf: 'center'
},
});
The solution to my issue was to encapsulate the drawer component in a native stack component. The 'Hamburger' icon works as expected, thanks for all the help and suggestions.
// Import Screens
import DrawRouter from './DrawRouter';
const Stack = createNativeStackNavigator<RootStackParamList>();
export default (): React.ReactElement => {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Main" component={DrawRouter} />
</Stack.Navigator>
</NavigationContainer>
);
};
Add the toogleDrawer() method to your onPress event on your Drawer.Navigator component:
onPress={()=> navigation.toggleDrawer()
I'm hoping to set up protected routes for my webapp built with React.js. I have an array 'menuOptions' that I have been looping through to add options to my navbar. However, I've now been running in to issues as I would like to add protected routes.
So for instance, I would like to have Home, Login and Register when the user is logged out, and Dashboard and Logout as the options for when the user is logged in. I'd really appreciate any help. Ideally, I'd somehow add the '!user' to the menuOptions array somehow.
My code is below (as you can see menuOptions does not do anything for the above at the moment):
import React, { useState, useContext } from "react";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, logout } from "../../firebase";
import { makeStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import Typography from "#material-ui/core/Typography";
import IconButton from "#material-ui/core/IconButton";
import MenuIcon from "#material-ui/icons/Menu";
import MenuItem from "#material-ui/core/MenuItem";
import Menu from "#material-ui/core/Menu";
import { useTheme } from "#material-ui/core/styles";
import useMediaQuery from "#material-ui/core/useMediaQuery";
const useStyles = makeStyles((theme) => ({
title: {
flexGrow: 1,
},
appbar: {
// background: 'none',
},
inactiveLink: {
color: "white",
padding: theme.spacing(1),
fontSize: "1.5rem",
},
activeLink: {
color: "black",
padding: theme.spacing(1),
fontSize: "1.5rem",
background: "#bfbfbf",
},
}));
function Navigation() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const open = Boolean(anchorEl);
const menuOptions = [
{ label: "Home", path: "/", },
{ label: "Register", path: "/register" },
{ label: "Dashboard", path: "/dashboard" },
{ label: "Logout", path: "/logout" },
];
const handleMenuSelect = (pageURL) => {
navigate(pageURL);
};
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const [user] = useAuthState(auth);
const navigate = useNavigate();
return (
<>
<AppBar
className={classes.appbar}
position="fixed"
elevation={0}
color="primary"
>
<Toolbar>
<Typography variant="h4" className={classes.title}>
The Football App
</Typography>
<Typography variant="h6" className={classes.title}>
All you ever wanted to know about Football!
</Typography>
{isMobile ? (
<>
<IconButton
aria-label="menu"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={open}
onClose={() => setAnchorEl(null)}
>
{menuOptions.map((opt) => (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
))}
</Menu>
</>
) : (
<>
<nav>
<div>
{!user ? (
<NavLink
to="/"
className={({ isActive }) =>
isActive
}
>
Home
</NavLink>
) : (
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive
}
>
Dashboard
</NavLink>
)}
</div>
{!user ? (
<div>
<Link to="/login">
<a>Login</a>
</Link>
<Link to="/register">
<a>Register</a>
</Link>
</div>
) : (
<div className="navbar-end">
<Link to="/logout">
<a>Log Out</a>
</Link>
</div>
)}
</nav>
</>
)}
</Toolbar>
</AppBar>
</>
);
}
export default Navigation;
You could update your menuOptions object to include a protected key:
const menuOptions = [
{ label: "Home", path: "/", protected: false },
{ label: "Register", path: "/register", protected: false },
{ label: "Dashboard", path: "/dashboard", protected: false },
{ label: "Logout", path: "/logout", protected: true },
];
and update the logic where your render your menu options to something like
{menuOptions.map((opt) => {
if (opt.protected && !user) return;
return (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
);
}
)}
to only show certain options if the user exists
{menuOptions.filter((opt) => (user ? opt.protected : !opt.protected)).map((opt) => (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
)
)}
I have a CountryList react component
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function CountryList({
countries,
}: {
countries: any;
}): JSX.Element {
const [filter, setFilter] = React.useState("");
const [sortType, setSortType] = React.useState("");
console.log(filter);
const sorted = countries.sort((a: { name: string }, b: { name: any }) => {
const isReversed = sortType === "asc" ? 1 : -1;
return isReversed * a.name.localeCompare(b.name);
});
const onSort = (sortType: React.SetStateAction<string>) => {
console.log("changed");
setSortType(sortType);
};
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of countries</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
name="namePrefix"
style={{ padding: "0.35rem" }}
onChange={(e: any) => {
setFilter(e.target.value);
}}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<div className="btn-group">
<button
type="button"
className="btn dropdown-toggle sort-button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{sortType === "asc"
? "Ascending"
: sortType === "desc"
? "Descending"
: "Select"}
</button>
<ul className="dropdown-menu sort-button">
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("asc")}
>
Ascending
</button>
</li>
<li>
<button
className="dropdown-item"
type="button"
onClick={() => onSort("desc")}
>
Descending
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="country-list-items">
{countries &&
sorted.map((item: any, index: number) => (
<div key={index}>
<Link style={{ display: "block" }} to={`/regions`}>
{item.name}
</Link>
</div>
))}
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
{countries && countries.length > 10 ? (
<button className="secondary-button">Load More</button>
) : (
<p>There are no more countries</p>
)}
</div>
</div>
);
}
Now from this component I need to pass the data of selected country id while the user clicks on the Link of the respective country, which I will be able to get by {item.code}. Also on clicking the Link the user will be redirected to /regions route where the list of regions of the selected country from this component will be shown. This is the RegionList Component:
import React from "react";
import { Link } from "react-router-dom";
import { BsSearch } from "react-icons/bs";
export default function RegionList(): JSX.Element {
return (
<div style={{ marginTop: "3rem" }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "10px",
}}
>
<div>List of regions</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ position: "relative", marginRight: "1rem" }}>
<input
type="text"
placeholder="Filter"
style={{ padding: "0.35rem" }}
/>
<div style={{ position: "absolute", top: "5px", right: "5px" }}>
<BsSearch size="16" />
</div>
</div>
<div style={{ width: "8rem" }}>
<select name="sort" id="sort">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
</div>
<div className="country-list-items">
<div>
<Link style={{ display: "block" }} to={`/cities`}>
Alaska
</Link>
</div>
</div>
<div
style={{ marginTop: "20px", display: "flex", justifyContent: "center" }}
>
<button className="secondary-button">Load More</button>
<p>There are no more countries</p>
</div>
</div>
);
}
I need to pass the country id from the CountryList component to this RegionList component because I will do a GET network call in the RegionList component using the selected country id passed from the CountryList component. But I am not able to pass the country id data from CountryList component to RegionList component as they are on different routes and they do not have any common parent component. This is the route file for Countries
import { Route, Routes } from "react-router-dom";
import React from "react";
import CountryComponent from "../components/CountryComponent";
export class CountryRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<CountryComponent />} />
</Routes>
);
}
}
here <CountryComponent /> is the mother component of CountryList
This is the route file for Regions:
import { Route, Routes } from "react-router-dom";
import React from "react";
import RegionComponent from "../components/RegionComponent";
export class RegionsRoute extends React.Component {
render() {
return (
<Routes>
<Route path="/" element={<RegionComponent />} />
</Routes>
);
}
}
here <RegionComponent /> is the mother component of RegionList
Here is the Main Component where all the components are called
import React from "react";
import { Routes, Route } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import styled from "styled-components";
import "styled-components/macro";
import { CountryRoute } from "../country/route";
import { RegionsRoute } from "../region/route";
import { CitiesRoute } from "../cities/route";
const MainContainer = styled.div`
min-height: 100%;
margin: 5rem;
`;
export const Main = (): JSX.Element => {
return (
<>
<>
<MainContainer>
<div style={{ textAlign: "center" }}>
<b>GEO SOFTWARE</b>
</div>
<div>
<div>
<Routes>
<Route path={"/countries*"} element={<CountryRoute />} />
<Route path={"/regions*"} element={<RegionsRoute />} />
<Route path={"/cities*"} element={<CitiesRoute />} />
</Routes>
</div>
</div>
<ToastContainer
toastClassName={"toastContainer e-12"}
hideProgressBar
position="bottom-left"
closeButton={false}
autoClose={5000}
bodyClassName={"toastBody"}
/>
</MainContainer>
</>
</>
);
};
Now how can I pass the selected country code data from CountryList to the RegionList component.
You can use Query Params for this. In the CountryList you can use the Link like this:
<Link style={{ display: "block" }} to={`/regions?country=COUNTRY_ID`}>
Then in the RegionsList youn can get that Query Parameter from the url and use as you want.
Check this example https://reactrouter.com/web/example/query-parameters
You could set up a simple "store" to keep track of the selected country independently of your component hierarchy.
The simplest possible store
A stripped down, simplest implementation possible might look something like this:
const data = {}
export default {
setCountry: c => data.country = c,
getCountry: () => data.country
}
Because the "store" data is a singleton, any component that imports the store will get the same info, regardless of where it is in the component tree.
import store from './store';
export default () => (
<div>{store.getCountry()}</div>
)
Listening for changes, etc.
The example above omits some details that may be important, depending on what you're doing, like updating views that have already rendered when the country value changes.
If you need that sort of thing you could make the store an event emitter so your components can listen for updates:
import Emitter from 'events';
class CountryStore extends Emitter {
data = {}
getCountry () {
return this.data.country;
}
setCountry (c) {
this.data.country = c;
this.emit('change'); // notify interested parties of the change
}
}
export default new CountryStore();
With the emitter in place, components can register for change notifications when they mount:
import store from './store';
function SomeComponent () {
useEffect(() => {
store.on('change', () => {
// do stuff when store changes happen
}, [])
})
return (<div>...</div>)
}
Custom Hook
To make it easy to do this wherever its needed you could wrap it all up in a custom hook that handles it all and returns the current value and a setter [country, setCountry] just like useState would:
const useCountry = () => {
const [country, setCountry] = useState(store.getCountry());
const handler = () => setCountry(store.getCountry());
useEffect(() => {
store.on('change', handler);
return () => store.off('change', handler);
})
return [country, c => store.setCountry(c)];
}
Then your components have it easy:
import useCountry from './useCountry.js';
export default function SomeComponent () {
const [country, setCountry] = useCountry();
return (
<div>
<div>Current Country: {country}</div>
<button onClick={() => setCountry(Math.random())}>Change Country</button>
</div>
)
}
There are off-the-shelf libraries that will do all of this and more for you, but I thought it might be more helpful to explain an actual rudimentary implementation.
You can have some sort of global state country_id which is initially equal to null.
When user clicks on a country, set that country_id to be equal to the clicked country id.
Now, Inside you RegionList component you can access the country id through country_id state.
You can achieve the state management by different ways:
Prop drilling
Context API
Use Redux or Recoil to handle state-management
As others have pointed out, this is 100% what context is for.
It looks like this:
import React, { createContext, useContext } from 'react';
const MyCountryContext = createContext(null);
export const useCountry = () => useContext(MyCountryContext);
export const MyCountryContext = ({children}) => {
const [country,setCountry] = useState();
return (
<MyCountryContext.Provider value={[country,setCountry]}>
{children}
</MyCountryContext.Provider>
)
}
Use it like this:
export const Main = (): JSX.Element => {
return (
<MyCountryContext>
...rest of your tree
</MyCountryContext>
);
}
Then, in any components that are below MyCountryContext you can use the hook just like useState:
import { useCountry } from './MyCountryContext';
const MyComponentThatUsesCountry = () => {
const [country,setCountry] = useCountry();
return (...)
}
I'm trying to build an app with React and Redux (DVA). It's using Ant.Design as the main framework. I'm trying to change the URL when the user clicks on a button, and obviously 'bind' that url change to an action, so that if the user goes directly to that URL, he gets what he wants.
At the moment here's what I have, in a function in my component.
const { dispatch, match } = this.props;
dispatch(routerRedux.push('/f/' + record.id));
This is the only thing that I was able to produce. It correctly changes the url, but doesn't bind the url with a specific behaviour, making it completely useless.
How do I link the URL with an action?
If you wish to trigger an action based on a URL, you'll need to use react-router to route a component that then performs the desired action. In such a case it is also a good idea to then visit a different URL, erasing the action-URL from the browser's history.
A typical router definition might look something like this (taken from react-router-redux's docs):
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/success" component={Success}/>
</div>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
)
So you wish to add a path /f/<record-id>. You can do this by adding a line like this:
<Route path="/f/:recordId" component={MyActionComponent}/>
Now you need to define a component, MyActionComponent, that will perform your action.
import { connect } from 'react-redux';
import { replace } from 'react-router-redux';
const mapDispatchToProps = (dispatch: Dispatch) => ({
visitNextLocation: () => dispatch(replace('/success')),
myAction: (recordId) => dispatch(myAction(recordId)),
});
const withDispatch = connect(null, mapDispatchToProps);
class MyActionComponent extends Component {
props: {
match: {
params: {
recordId: string,
}
},
redirectToLogin: () => void,
myAction: string => void,
};
componentWillMount() {
const recordId = this.props.match.params.recordId;
if (recordId) {
this.props.myAction(token);
this.props.visitNextLocation();
}
}
render() {
return null;
}
}
Note the use of replace instead of push. This means, when a user visits this URL their action will get performed and they'll end up on /success. But if they click the Back button, they won't then revisit this URL and run the action again.
I can't put the code on Codepen for privacy reasons. But here's an extract:
router.js
...
},
'/users': {
component: dynamicWrapper(app, ['rule'], () => import('../routes/users')),
},
'/f/:userID': {
component: dynamicWrapper(app, ['rule'], () => import('../routes/users')),
},
...
users.js (the main component that contains LeftPanel and RightPanel)
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, List, Divider, Badge, Select, Radio, Input, Popover, Button, Table, Spin } from 'antd';
import RightPanel from './RightPanel';
import LeftPanel from './LeftPanel';
import { routerRedux, Route, Switch } from 'dva/router';
import 'font-awesome/css/font-awesome.min.css';
import FadeIn from 'react-fade-in';
#connect(({ rule, loading }) => ({rule, loading: loading.models.rule }))
export default class Users extends React.Component {
constructor(props) {
super(props)
this.state = {
selected_user: [],
defaultView: true,
isLoadingNow: false,
selectedRowKeys: [],
makeEmpty: false,
searchRes: []
}
}
selectRow = (record) => {
const { dispatch, match } = this.props;
dispatch(routerRedux.replace({ pathname: '/f/' + record.id }));
this.setState({isLoadingNow: true, selectedRowKeys: record.key})
setTimeout(() => {
this.setState({
isLoadingNow: false,
defaultView: false,
selected_user: record
})
}, 75)
}
componentDidMount() {
const { dispatch, match } = this.props;
dispatch({
type: 'rule/fetch'
});
if (match.params.userID == undefined) {
// do nothing
} else if (match.params.userID) {
var result = this.props.rule.data.list.filter(function( obj ) {
return obj.id == match.params.userID;
});
this.selectRow.bind(this, result[0])
}
}
render() {
const { selectedRowKeys } = this.state;
const rowSelection = {
selectedRowKeys,
type:"radio"
};
const { rule: { data }, loading } = this.props;
return (<div>
<LeftPanel
rowSelection={rowSelection}
dataSource={this.state.makeEmpty ? this.state.searchRes : this.props.rule.data.list}
selectRow={this.selectRow}
loadingStatus={loading}
/>
<RightPanel
selected_user={this.state.selected_user}
is_default={this.state.defaultView}
loading={this.state.isLoadingNow}
/>
</div>);
}
}
leftPanel.js (responsible for displaying the list of links, on which the user will click on one, which will do 2 things:
- change the url accordingly
- display specific data on RightPanel.js)
import React from 'react';
import { Table, Card } from 'antd';
import styles from './index.less';
// import 'font-awesome/css/font-awesome.min.css';
import { Row, Col, List, Divider, Badge, Select, Radio, Input, Popover, Button } from 'antd';
var moment = require('moment');
class LeftPanel extends React.Component {
constructor(props) {
super(props)
this.state = {
selected_row_index: undefined
}
}
handleChangeStyleOnSelectRow(index) {
this.setState({
selected_row_index: index
}, console.log(this.state.selected_row_index))
}
prettifyForTable(raw_data) {
var prettyRows = [];
raw_data.map((item,index) =>
prettyRows.push(
<div style={{"width": "100%"}}>
<Row style={{ "align-items": "center"}} type="flex" justify="space-between">
<Col span={10}>
<div style={{"font-size": "15px", "text-align": "center"}}>
{item.user_name} <i style={{"color": "rgba(0, 0, 0, 0.25)", "margin": "0 10px", "transform": "rotate(45deg)"}} className="fa fa-plane"> </i> {item.user_age}
<div style={{"font-size": "12px", "color": "grey"}}> {moment(item.user_color).format('HH:MM')} · {moment(item.user_order).format('HH:MM')} </div>
</div>
</Col>
<Col span={3}>
<div style={{"text-align": "right", "text-align": "center"}}>
{item.user_family}
</div>
</Col>
<Col span={6}>
<div style={{"text-align": "right", "text-align": "center"}}>
{moment(item.user_height).format('MMMM D')}
</div>
</Col>
<Col span={3}>
<div style={{"text-align": "center"}}>
{(item.status == "in_progress") ? <div> <Badge style={{"padding-right": "25px"}} status="processing"/></div> : <div style={{"text-align": "center"}}> <Badge style={{"padding-right": "25px"}} status="default"/></div>}
</div>
</Col>
</Row>
</div>
)
);
return prettyRows;
}
render() {
const stylesSelectedRow = { "background": "rgba(155,155,155,0.05)", "box-shadow": "0 0 5px 0 #4A90E2", "transform": "scale(1.01)"};
const { dataSource } = this.props;
return(
<div>
{dataSource &&
<Card bordered={false} loading={this.props.loadingStatus} className={styles.userRows} bodyStyle={{"padding": "0 15px"}}>
<List
size="small"
bordered={false}
dataSource={this.prettifyForTable(dataSource)}
renderItem={(item, index) => (<List.Item onClick={() => {this.state.selected_row_index == index ? null : this.props.selectRow(this.props.dataSource[index]); this.handleChangeStyleOnSelectRow(index)}} style={this.state.selected_row_index == index ? stylesSelectedRow : null} className={styles.userRows}>{item}</List.Item>)}
/>
</Card>
}
</div>
)
}
}
export default LeftPanel;
and finally RightPanel.js, that is reponsible for listening to the URL or a click on LeftPanel, and display data accordingly.
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import infopng from '../../../assets/info.svg';
import TabInformations from './TabInformations.js';
import TabFamily from './TabFamily.js';
import TabProblems from './TabProblems.js';
import { Tabs, Button, Spin, Icon, Table, Pagination, Card, Col, Row, Spinner, Badge } from 'antd';
const TabPane = Tabs.TabPane;
import 'font-awesome/css/font-awesome.min.css';
import WindowSizeListener from 'react-window-size-listener'
import FadeIn from 'react-fade-in';
export default class RightPanel extends Component {
render() {
if (this.props.loading) {
return(
<div>
<Spin
indicator={<Icon type="loading" style={{ fontSize: 24 }} spin />}
src="http://web.gndu.ac.in/DepartmentProfile/images/spinner.gif"
/>
</div>
);
} else if (this.props.is_default) {
return(
<div style={{"margin-top": "64px", "margin-right": "10px", "text-align": "center", "height": "90vh", "display": "flex", "align-items": "center", "justify-content": "center"}}>
<div>
<img src={infopng} style={{"height": "155px"}} />
<p style={{"color": "#8e8e8e"}}> select a user on the <br/> left-hand side... </p>
</div>
</div>
);
} else {
return (
<FadeIn>
<Card bodyStyle={{"padding": "0"}} style={{"background-color": "white", "height":"90vh", "padding": "20px", "box-shadow": "rgba(0, 21, 41, 0.1) 0px 0px 6px", "opacity": "1", "transition": "background 0.6s", "border-radius": "2px", "margin": "10px 10px 0 0px", "margin-top": "64px"}}>
<Tabs defaultActiveKey="1" style={{"text-align": "center", "padding": "0 15px"}}>
<TabPane tab="General" key="1">
<TabInformations
selected_user={this.props.selected_user}
/>
</TabPane>
<TabPane tab="Servicing" key="2">
<TabFamily
selected_user={this.props.selected_user}
/>
</TabPane>
<TabPane tab={<div>Defect(s)<Badge showZero count={0} style={{ backgroundColor: '#fff', color: '#999', boxShadow: '0 0 0 1px #d9d9d9 inset', "margin-left": "10px" }} /></div>} key="3">
<TabProblems />
</TabPane>
</Tabs>
</Card>
</FadeIn>
);
}
}
}
This code does the initial job pretty well: when the user clicks on a link on leftPanel.js, leftPanel.js calls the method selectRow in users.js, which in turn selects a row and display it on RightPanel.js.
My question: how to add to this the fact that it changes the URL whenever the user clicks? And obviously, if the user clicks on "go back" in Chrome, the data in RightPanel.js has to change accordingly.. ?
When I add the <Menu /> component to my header as below:
let SearchPage = (props) => {
const menu = (
<Container>
<Header style={styles.header}>
<Left>
<Button >
<Menu />
</Button>
</Left>
<Body>
I get the error
Maximum call stack size exceeded
and of course if I comment out the <Menu /> line in my SearchPage, there is no error.
The menu is a react-native-off-canvas-menu
My menu component:
components/menu/Menu.js
import React from 'react'
import { View, Icon } from 'react-native'
import { connect } from 'react-redux'
import { togglePageMenu } from './menu.action'
import { OffCanvas3D } from 'react-native-off-canvas-menu'
//import AddPage from '../add-page/AddPage'
import SearchPage from '../search-page/SearchPage'
const mapStateToProps = (state) => ({
isOpen: state.get('menu').isOpen
})
const mapDispatchToProps = (dispatch) => ({
togglePageMenu: () => {
dispatch(togglePageMenu())
}
})
let Menu = (props) => (
<View style={{ flex: 1 }}>
<OffCanvas3D
active={props.isOpen}
onMenuPress={props.togglePageMenu}
backgroundColor={'#222222'}
menuTextStyles={{ color: 'white' }}
handleBackPress={true}
menuItems={[
{
title: 'Search Products',
icon: <Icon name="camera" size={35} color='#ffffff' />,
renderScene: <SearchPage />
}
]} />
</View>
)
Menu.propTypes = {
togglePageMenu: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired
}
Menu = connect(
mapStateToProps,
mapDispatchToProps
)(Menu)
export default Menu
What could be causing the error?
Here is my component that uses the menu (probably not relevant):
components/search-page/SearchPage.js
import { ScrollView, StyleSheet, View } from 'react-native'
import {
Container,
Button,
Text,
Header,
Body,
Right,
Left,
Title,
Icon
} from 'native-base'
import React from 'react'
import Keywords from '../keywords/Keywords'
import Categories from '../categories/Categories'
import Location from '../location/Location'
import Menu from '../menu/Menu'
import DistanceSlider from '../distanceSlider/DistanceSlider'
import Map from '../map/Map'
import Drawer from 'react-native-drawer'
import { connect } from 'react-redux'
import { toggleMenu } from './searchPage.action'
import { styles, wideButtonColor } from '../../style'
import searchPageStyle from './style'
import { selectIsSearchFormValid } from './isSearchFormValid.selector'
const mapStateToProps = (state) => ({
isMenuOpen: state.get('searchPage').get('isMenuOpen'),
isSearchFormValid: selectIsSearchFormValid(state)
})
const mapDispatchToProps = (dispatch) => ({
toggleMenu: () => {
dispatch(toggleMenu())
}
})
let SearchPage = (props) => {
const menu = (
<Container>
<Header style={styles.header}>
<Left>
<Button >
<Menu />
</Button>
</Left>
<Body>
<Title style={styles.title}>Search Products</Title>
</Body>
<Right>
</Right>
</Header>
<Container style={styles.container}>
<ScrollView keyboardShouldPersistTaps={true}>
<Categories />
<View style={searchPageStyle.locationContainer}>
<Location />
</View>
<DistanceSlider />
<Keywords />
<Button
block
style={{
...searchPageStyle.goButton,
backgroundColor: wideButtonColor(!props.isSearchFormValid)
}}
disabled={!props.isSearchFormValid}
onPress={props.toggleMenu}>
<Text>GO</Text>
</Button>
</ScrollView>
</Container>
</Container>
)
return (
<Drawer open={props.isMenuOpen} content={menu}>
<Container style={mapStyles.container}>
<Map />
</Container>
</Drawer>
)
}
SearchPage.propTypes = {
toggleMenu: React.PropTypes.func.isRequired,
isMenuOpen: React.PropTypes.bool.isRequired,
isSearchFormValid: React.PropTypes.bool.isRequired
}
SearchPage = connect(
mapStateToProps,
mapDispatchToProps
)(SearchPage)
export default SearchPage
const mapStyles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
height: 400,
width: 400,
justifyContent: 'flex-end',
alignItems: 'center',
}
})
It's because you are rendering another <SearchPage /> within your menu: renderScene: <SearchPage />. This creates a circular dependency where a SearchPage is creating a Menu and the Menu is creating a SearchPage... etc. Until, as you saw, you run out of memory.