I'm trying to learn react and fairly new to the framework. I am trying to create a simple navbar component wih material-ui that is responsive (will show all links on medium devices and up, and open a side drawer on small devices). I have most of it setup to my liking, however, the issue I am currently having, is getting and setting the active link according to the page I am on.
It seems to works correctly on the medium devices and up, but when transitioning to a smaller device, the link is not updated correctly, as it will keep the active link from the medium screen set, while updating the side drawer active link.
Navbar.js
const Navbar = () => {
const classes = useStyles();
const pathname = window.location.pathname;
const path = pathname === '' ? '' : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
return (
<>
<HideNavOnScroll>
<AppBar position="fixed">
<Toolbar component="nav" className={classes.navbar}>
<Container maxWidth="lg" className={classes.navbarDisplayFlex}>
<List>
<ListItem
button
component={RouterLink}
to="/"
selected={selectedItem === ''}
onClick={event => handleItemClick(event, '')}
>
<ListItemText className={classes.item} primary="Home" />
</ListItem>
</List>
<Hidden smDown>
<List
component="nav"
aria-labelledby="main navigation"
className={classes.navListDisplayFlex}
>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === 'account/login'}
onClick={event => handleItemClick(event, 'account/login')}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/register"
selected={selectedItem === 'account/register'}
onClick={event => handleItemClick(event, 'account/register')}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</Hidden>
<Hidden mdUp>
<SideDrawer />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</HideNavOnScroll>
<Toolbar id="scroll-to-top-anchor" />
<ScrollToTop>
<Fab aria-label="Scroll back to top">
<NavigationIcon />
</Fab>
</ScrollToTop>
</>
)
}
SideDrawer.js
const SideDrawer = () => {
const classes = useStyles();
const [state, setState] = useState({ right: false });
const pathname = window.location.pathname;
const path = pathname === "" ? "" : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
const toggleDrawer = (anchor, open) => (event) => {
if (
event &&
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const drawerList = (anchor) => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer(anchor, false)}
onKeyDown={toggleDrawer(anchor, false)}
>
<List component="nav">
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/login"}
onClick={(event) => handleItemClick(event, "account/login")}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/register"}
onClick={(event) => handleItemClick(event, "account/register")}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</div>
);
return (
<React.Fragment>
<IconButton
edge="start"
aria-label="Menu"
onClick={toggleDrawer("right", true)}
>
<Menu fontSize="large" style={{ color: "white" }} />
</IconButton>
<Drawer
anchor="right"
open={state.right}
onClose={toggleDrawer("right", false)}
>
{drawerList("right")}
</Drawer>
</React.Fragment>
);
};
Code Sandbox - https://codesandbox.io/s/async-water-yx90j
I came across this question on SO: Is it possible to share states between components using the useState() hook in React?, which suggests that I need to lift the state up to a common ancestor component, but I don't quite understand how to apply this in my situation.
I would suggest to put aside for a moment your code and do a playground for this lifting state comprehension. Lifting state is the basic strategy to share state between unrelated components. Basically at some common ancestor is where the state and setState will live. there you can pass down as props to its children:
const Parent = () => {
const [name, setName] = useState('joe')
return (
<>
<h1>Parent Component</h1>
<p>Child Name is {name}</p>
<FirstChild name={name} setName={setName} />
<SecondChild name={name} setName={setName} />
</>
)
}
const FirstChild = ({name, setName}) => {
return (
<>
<h2>First Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Mary')}>My Name is Mary</button>
</>
)
}
const SecondChild = ({name, setName}) => {
return (
<>
<h2>Second Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Joe')}>My Name is Joe</button>
</>
)
}
As you can see, there is one state only, one source of truth. State is located at Parent and it passes down to its children. Now, sometimes it can be troublesome if you need your state to be located at some far GreatGrandParent. You would have to pass down each child until get there, which is annoying. if you found yourself in this situation you can use React Context API. And, for most complicated state management, there are solutions like redux.
Related
I'm using react-sidebar-pro for my sidebar component in my react app and I want my sidebar menu items' active state to turn true when the user has clicked and is redirected to a link. So far what I've done is set a parameter if the user is in a specific address it will turn true but it only updates when I refresh the page. How do I update it without having to refresh the page so it would look responsive? I thought of using useState to resolve this but I have no idea on how to apply it to the code because I'm still learning React
Here's my Sidebar.js
function Sidebar() {
//create initial menuCollapse state using useState hook
const [menuCollapse, setMenuCollapse] = useState(false)
//create a custom function that will change menucollapse state from false to true and true to false
const menuIconClick = () => {
//condition checking to change state from true to false and vice versa
menuCollapse ? setMenuCollapse(false) : setMenuCollapse(true);
}
return (
<>
<div id="header">
{/* collapsed props to change menu size using menucollapse state */}
<ProSidebar collapsed={menuCollapse}>
<SidebarHeader>
<div className="logotext">
{/* small and big change using menucollapse state */}
<p>{menuCollapse ? "LOGO" : "LONG LOGO"}</p>
</div>
<div className="closemenu" onClick={menuIconClick}>
{/* changing menu collapse icon on click */}
{menuCollapse ? (
<FiArrowRightCircle/>
) : (
<FiArrowLeftCircle/>
)}
</div>
</SidebarHeader>
<SidebarContent>
<Menu iconShape="square">
<MenuItem active={window.location.pathname === "/"} icon={<FiHome />} >
Home
<Link to="/"/>
</MenuItem>
<MenuItem active={window.location.pathname === "/FAQ"} icon={<FaList />}>
FAQ
<Link to="/FAQ"/>
</MenuItem>
<MenuItem active={window.location.pathname === "/Search-sec"} icon={<RiPencilLine />}>SEARCH SEC<Link to="/Search-sec"/></MenuItem>
</SidebarContent>
<SidebarFooter>
<Menu iconShape="square">
<MenuItem icon={<FiLogOut />}>Logout</MenuItem>
</Menu>
</SidebarFooter>
</ProSidebar>
</div>
</>
)
}
export default Sidebar
Basically two options here:
make window.location.path as your local state
please refer to Why useEffect doesn't run on window.location.pathname changes?
add another state (changed based on click), to force component re-render
function Sidebar() {
const [menuCollapse, setMenuCollapse] = useState(false);
const [activeIndex, setActiveIndex] = useState(() => {
const initialIndex =
window.location.pathname === '/' ? 0
: window.location.pathname === '/FAQ' ? 1
: window.location. pathname === 'Search-sec' ? 2
: 0;
return initialIndex;
});
const menuIconClick = () => {
setMenuCollapse(!menuCollapse);
};
return (
<>
<div id="header">
{/* collapsed props to change menu size using menucollapse state */}
<ProSidebar collapsed={menuCollapse}>
<SidebarHeader>
<div className="logotext">
{/* small and big change using menucollapse state */}
<p>{menuCollapse ? "LOGO" : "LONG LOGO"}</p>
</div>
<div className="closemenu" onClick={menuIconClick}>
{/* changing menu collapse icon on click */}
{menuCollapse ? (
<FiArrowRightCircle />
) : (
<FiArrowLeftCircle />
)}
</div>
</SidebarHeader>
<SidebarContent>
<Menu iconShape="square">
<MenuItem active={activeIndex === 0} icon={<FiHome />} >
Home
<Link id="MenuItemHome" to="/" onClick={() => setActiveIndex(0)} />
</MenuItem>
<MenuItem active={activeIndex === 1} icon={<FaList />} >
FAQ
<Link id="MenuItemFAQ" to="/FAQ" onClick={() => setActiveIndex(1)} / >
</MenuItem>
<MenuItem active={activeIndex === 2} icon={<RiPencilLine />}>
SEARCH SEC
<Link id="MenuItemSearch" to="/Search-sec" onClick={() => setActiveIndex(2)} />
</MenuItem>
</Menu>
</SidebarContent>
<SidebarFooter>
<Menu iconShape="square">
<MenuItem icon={<FiLogOut />}>Logout</MenuItem>
</Menu>
</SidebarFooter>
</ProSidebar>
</div>
</>
);
}
export default Sidebar;
I'm using this excellent example (Nested sidebar menu with material ui and Reactjs) to build a dynamic nested menu for my application. On top of that I'm trying to go one step further and put it into a Material UI appbar/temporary drawer. What I'd like to achieve is closing the drawer when the user clicks on one of the lowest level item (SingleLevel) however I'm having a tough time passing the toggleDrawer function down to the menu. When I handle the click at SingleLevel I consistently get a 'toggle is not a function' error.
I'm relatively new to this so I'm sure it's something easy and obvious. Many thanks for any answers/comments.
EDIT: Here's a sandbox link
https://codesandbox.io/s/temporarydrawer-material-demo-forked-v11ur
Code is as follows:
Appbar.js
export default function AppBar(props) {
const [drawerstate, setDrawerstate] = React.useState(false);
const toggleDrawer = (state, isopen) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setDrawerstate({ ...state, left: isopen });
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" color="secondary">
<Toolbar>
<IconButton
size="large"
edge="start"
color="primary"
aria-label="menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<img src={logo} alt="logo" />
</Toolbar>
<Drawer
anchor='left'
open={drawerstate['left']}
onClose={toggleDrawer('left', false)}
>
<Box>
<AppMenu toggleDrawer={toggleDrawer} />
</Box>
</Drawer>
</AppBar>
</Box >
)
}
Menu.js
export default function AppMenu(props) {
return MenuItemsJSON.map((item, key) => <MenuItem key={key} item={item} toggleDrawer={props.toggleDrawer} />);
}
const MenuItem = ({ item, toggleDrawer }) => {
const MenuComponent = hasChildren(item) ? MultiLevel : SingleLevel;
return <MenuComponent item={item} toggleDrawer={toggleDrawer} />;
};
const SingleLevel = ({ item, toggleDrawer }) => {
const [toggle, setToggle] = React.useState(toggleDrawer);
return (
<ListItem button onClick={() => { toggle('left', false) }}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} secondary={item.description} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
You shouldn't call a react hook inside of any function that is not a react component. Please see React Rules of Hooks
What you could do instead is pass setToggle directly into the Drawer component as a prop and do something like this for it's onClick attribute:
onClick={() => setToggle(<value>)}
How do I put a button inside the Tab? If I'll click Homepage, it does go in the correct tab with the correct line indicator. However, if I'll click "Profile", the indicator will go to the log out button. How do I correct this where the indicator will stay on the correct tab? This is what it looks like if I'll go to the Profile Tab, the indicator will directly go to the Logout button.
const Header = (props) => {
const { currentUser } = props;
const [value, setValue] = React.useState(0);
const [open, setOpen] = React.useState(false);
const handleChange = (event, newValue) => {
setValue(newValue);
};
// for the tab to stay on the correct path/page even if it was reloaded
useEffect(() => {
let path = window.location.pathname;
if (path === "/" && value !== 0) setValue(0);
else if (path === "/login" && value !== 1) setValue(1);
else if (path === "/registration" && value !== 2) setValue(2);
else if (path === "/profile" && value !== 2) setValue(2);
}, [value]);
const isMatch = useMediaQuery(theme.breakpoints.down("md"));
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<AppBar>
{/* */}
<Toolbar
variant="dense">
{isMatch ? (
<h1>
<div>
<MobileviewComponent />
</div>
</h1>
) : (
<div>
<Grid>
<Tabs
value={value}
onChange={handleChange}
>
{currentUser && (
<Tab
disableRipple
label="Homepage"
to="/"
component={Link}
/>
)}
{currentUser && (
<Tab
disableRipple
label="Profile"
to="/profile"
component={Link}
/>
)}
{currentUser && (
<Button color="inherit" onClick={handleClickOpen}>
Logout
</Button>
)}
{!currentUser && (
<Tab
disableRipple
label="Homepage"
to="/"
component={Link}
/>
)}
{!currentUser && (
<Tab
disableRipple
label="Login"
to="/login"
component={Link}
/>
)}
</Tabs>
</Grid>
</div>
)}
</Toolbar>
</AppBar>
</div>
);
};
you are setting state as 2 for both /registration and /profile so when you click on /profile tab value is set to 2 and I guess you are material-ui so the tabs count starts from 0, hence 2 indicates 3rd item in tabs and that is your logout button, hence it is highlighted
I need basic help.
I have an array. When i click some link on the screen, i want to see just their page, i mean him/her name, not all of them.
First, user page has all of the users. If i click some of them, i want to go her/his page. How can i do this?
Users has userid, so it may help you.
ProfilePage codes: (it gives all of the users with item.name)
const ProfilePage = () => {
const { posts, users } = useContext(UserContext);
return (
<>
{users.map((item, index) => {
return (
<>
<h1>{item.name}</h1>
</>
);
})}
</>
);
};
Users page codes:
return (
<>
{users.map((item, index) => {
return (
<>
<a href={`/profile/${item.username}`}>
<List className={classes.root}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar alt="Remy Sharp" />
</ListItemAvatar>
<ListItemText
primary={item.email}
secondary={
<React.Fragment>
<Typography
component="span"
variant="body2"
className={classes.inline}
color="textPrimary"
>
{item.name}
</Typography>
{<br />}
{item.username}
</React.Fragment>
}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
</a>
</>
);
})}
</>
);
Users page image:
Profile page image: (first users page. It should show just 1 name not all of them, i know i used map its wrong)
const ProfilePage = () => {
const { posts, users } = useContext(UserContext);
const { name } = useParams();//Your dynamic route name. Example: <Route path="/profile/:name">
const filteredUsers = users.filter(item => item.name === name);//Find all users with name Bret
//Ideally, you need to make a router with a dynamic ID, then there will be a unique user. Example: <Route path="/profile/:id">
//const { id } = useParams();
//const filteredUsers = users.filter(item => item.id === id);
return (
<>
{filteredUsers.map((item, index) => {
return (
<>
<h1>{item.name}</h1>
</>
);
})}
</>
);
};
I am creating the following component:
It will contain an array of objects, where each object is a prescription, with the medicine name from the select and a TextField for the Dosis.
My problem is that the TextField loses focus on every onChange() and is very frustrating because it cannot be edited on a single focus.
This is my component :
const MedicineSelect = ({ medications, setMedications, ...props }) => {
const { medicines } = useMedicines()
const classes = useStyles()
const handleChange = (index, target) => {
// setAge(event.target.value)
const newMedications = cloneDeep(medications)
newMedications[index][target.name] = target.value
setMedications(newMedications)
}
const handleAddMedicine = () => {
const newMedications = cloneDeep(medications)
newMedications.push({ medicine: '', dosis: '', time: '' })
setMedications(newMedications)
}
const handleDeleteMedicine = (index) => {
console.log('DELETE: ', index)
const newMedications = cloneDeep(medications)
newMedications.splice(index, 1)
setMedications(newMedications)
}
return (
<Paper style={{ padding: 5 }}>
<List>
{medications.map((medication, index) => (
<ListItem key={nanoid()} divider alignItems='center'>
<ListItemIcon>
<Tooltip title='Eliminar'>
<IconButton
className={classes.iconButton}
onClick={() => handleDeleteMedicine(index)}
>
<HighlightOffOutlinedIcon />
</IconButton>
</Tooltip>
</ListItemIcon>
<FormControl className={classes.formControl}>
<InputLabel
id={`${index}-select-${medication}-label`}
>
Medicamento
</InputLabel>
<Select
labelId={`${index}-select-${medication}-label`}
id={`${index}-select-${medication}`}
name='medicine'
value={medication.medicine}
onChange={(event) =>
handleChange(index, event.target)
}
>
{medicines.map((medicine) => (
<MenuItem
key={nanoid()}
value={medicine.name}
>
{medicine.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
// fullWidth
id={`${index}-text-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>
</ListItem>
))}
<Button onClick={handleAddMedicine}>+ agregar</Button>
</List>
</Paper>
)
}
And here is where I set the component:
const [medications, setMedications] = useState([
{ medicine: '', dosis: '', time: '' },
])
...
<Grid item md={12} xs={12}>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
>
<Typography variant='h4'>
Tratamiento:
</Typography>
</AccordionSummary>
<AccordionDetails>
<Container disableGutters>
<MedicineSelect
medications={medications}
setMedications={setMedications}
/>
</Container>
</AccordionDetails>
</Accordion>
</Grid>
...
Adding and removing objects from the array works perfect. selecting the medicine from the select, also works perfect. the only problem I have is when editing the Dosis TextField, with every character, the focus is lost and I have to click again on the TextField.
Please help me getting this fixed!!!
After searching a lot, finally I found the solution. Actually when using nanoid() to create unique keys, on every state update React re-renders all components and since the id of both the List and the TextField component are regenerated by nanoid on every render, React loses track of the original values, that is why Focus was lost.
What I did was keeping the keys unmuttable:
<ListItem key={`medication-${index}`} divider alignItems='center'>
and
<TextField
key={`dosis-${index}`}
fullWidth
// id={`${index}-dosis-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>