State is undefined when used in react router link inside useMemo hook - reactjs

I am using react router to navigate to another page and pass state to that page using this inside useMemo
const columns = React.useMemo(
() => [
{
Header: "Actions",
id: "actions",
Cell: (tableProps) => {
return (
<>
<Link
to={{
pathname: "list-table/list-table-edit",
state: { selectedRow },
}}
>
<span
style={{
cursor: "pointer",
color: "grey",
textDecoration: "underline",
}}
onClick={(event) => {
setSelectedID(tableProps.row.original.asset_ID);
}}
>
<Tooltip title="Edit Row">
<EditButton aria-label="edit">
<CreateIcon fontSize="small" />
</EditButton>
</Tooltip>
</span>
</Link>
</>
);
},
},
...MakeTableColumns,
],
[selectedRow]
);
My state are declared like this
const [selectedID, setSelectedID] = useState();
const selectedRow = oriData.find((row) => row.asset_ID === selectedID);
when user click Edit, it will set row ID to selectedID and selectedRow finds row values and pass to list-table-edit page.
however, when I console.log props.location.state from list-table-edit , it's showing undefined. When I take out <Link> .. outside of useMemo and give a status id, it is working fine, I think it's something wrong with useMemo.
If anyone could help, I would be very much appreciated, thanks
Updated
Now I change from state: { selectedRow } to state: { tableProps.row.original } and pass to another page and it works without passing the state. I could have already had the selected row value by calling tableProps.row.original.

<Link to={{ pathname: '/list-table/list-table-edit', state: { record: selectedRow} }}>My route</Link>

Related

Next.js - how to change the component that is displayed?

I'm brand new to React/Nextjs.
I'm using this as a template:
https://ui.mantine.dev/component/navbar-simple
This is the example data:
const data = [
{ link: '/notifications', label: 'Notifications', icon: IconBellRinging },
{ link: '/billing', label: 'Billing', icon: IconReceipt2 },
{ link: '/security', label: 'Security', icon: IconFingerprint },
It is used to built a navbar:
export function NavbarSimple() {
const { classes, cx } = useStyles();
const [active, setActive] = useState('Billing');
const links = data.map((item) => (
<a
className={cx(classes.link, { [classes.linkActive]: item.label === active })}
href={item.link}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span>{item.label}</span>
</a>
));
return (
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{/*
I am trying to get the components to be swapped/updated here
*/}
</AppShell>
Goal: If someone clicks "Security" in the navbar, the Security component will load.
Let's say I have built the "Notifications", "Billing" and "Security" components.
To update DOM, I saw a guide for using react-router-dom to do this. But I am trying to stick with only Nextjs.
Whatever string is stored in "link" can be changed. But from the "link" in the data object, is there a way to update the component?
If someone can point me to a tutorial, example, or even what to search for, I'd greatly greatly appreciate it :) I've been researching this evening but have not found anything yet.
I also made a codesandbox: https://wytec9.csb.app/
You could have within this component a function that will bring the component you want depending on the value, a quick example for this:
const renderComponent = () => {
if(active === 'Billing'){
return <Billing/>
} else if (){
// you get the idea
}
}
Now call that function to bring up the right component:
return (
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{renderComponent()}
</AppShell>
You could achieve this by modifying your current data array of objects, and adding a component corresponding to each link.
Here is what it would looks like
const data = [
{ link: '/notifications', label: 'Notifications', icon: IconBellRinging, component: <Notification /> },
{ link: '/billing', label: 'Billing', icon: IconReceipt2, component: <Billing /> },
{ link: '/security', label: 'Security', icon: IconFingerprint, component: <Security /> }
]
Create a state that will store the component (you could modify your active state to be an object containing both the label and object):
const [activeComponent, setActiveComponent] = useState(null);
Then, update it in your onClick.
<a
className={cx(classes.link, { [classes.linkActive]: item.label === active })}
href={item.link}
key={item.label}
onClick={(event) => {
event.preventDefault();
setActive(item.label);
setActiveComponent(item.component)
}}
>
<item.icon className={classes.linkIcon} stroke={1.5} />
<span>{item.label}</span>
</a>
Good, you then can render the active component where you need it:
<AppShell
<Navbar>
<Navbar.Section>
{links}
</Navbar.Section>
</Navbar>
>
{activeComponent}
</AppShell>

Mobx only re-render item when computed value changes

I have a list of medias and my goal is to be able to show the currently playing media.
To do so, I compare the playing media ID with the one from the list to apply the correct style.
My issue is that when clicking on another item, all items re-render because they have a dependency on the playing media which is observable.
class AppStore {
...
get playingVideo() {
if (!this.player.videoId || this.player.isStopped) {
return null;
}
return this.videos[this.player.videoId];
}
}
const DraggableMediaItem = observer(({ video, index }) => {
const store = useAppStore();
const isMediaActive = computed(
() => store.playingVideo && video.id === store.playingVideo.id
).get();
console.log("RENDER", video.id);
const onMediaClicked = (media) => {
if (!isMediaActive) {
playerAPI.playMedia(media.id).catch(snackBarHandler(store));
return;
}
playerAPI.pauseMedia().catch(snackBarHandler(store));
};
let activeMediaProps = {};
if (isMediaActive) {
activeMediaProps = {
autoFocus: true,
sx: { backgroundColor: "rgba(246,250,254,1)" },
};
}
return (
<Draggable draggableId={video.id} index={index}>
{(provided, snapshot) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
button
disableRipple
{...activeMediaProps}
onClick={() => onMediaClicked(video)}
>
<Stack direction="column" spacing={1} sx={{ width: "100%" }}>
<Stack direction="row" alignItems="center">
<ListItemAvatar>
<MediaAvatar video={video} />
</ListItemAvatar>
<ListItemText primary={video.title} />
<ListItemText
primary={durationToHMS(video.duration)}
sx={{
textAlign: "right",
minWidth: "max-content",
marginLeft: "8px",
}}
/>
</Stack>
</Stack>
</ListItem>
)}
</Draggable>
);
});
I thought making isMediaActive a computed value would prevent that, but since the value the computation is based on changes, it triggers an update.
Is it possible to only re-render when the computed value changes ?
[EDIT]
Following #danila's comment, I cleaned up my code and injected the isActive parameter. However, I must still be missing something, since the List doesn't re-render when the player's video changes.
That would be the current pseudocode:
const MediaItem = observer(({ isActive }) => {
let activeMediaProps = {};
if (isActive) {
activeMediaProps = {
sx: { backgroundColor: "rgba(246,250,254,1)" },
};
}
return <ListItem {...activeMediaProps}> ... </ListItem>;
});
const Playlist = observer(() => {
const store = useAppStore();
const items = store.playlist;
return (
<List>
{items.map((item) => (
<MediaItem isActive={item.id === store.player.videoId} />
))}
</List>
);
});
[EDIT 2]
Code sandbox link with a working example:
https://codesandbox.io/s/silent-lake-2lvdc?file=/src/App.js
Thank you in advance for your help and time.
First of all you can't use computed like that. In most cases computed should be used like a property in your store. Similar to observable.
As for the question, if you don't want items to rerender you could provide this flag through props, something like that in pseudocode
const List = observer(() => {
return (
<div>
{items.map(item => (
<Item isMediaActive={store.playingVideo && item.id === store.playingVideo.id} />
))}
</div>
)
})
It is also better to have that list as "standalone" component, don't just render items inside your whole view. More info here https://mobx.js.org/react-optimizations.html#render-lists-in-dedicated-components
EDIT:
There is also another way, which is actually "more MobX" way of doing things, is to have isPlaying flag in the item object itself. But that might require you to change how you work with your data, so the first example is probably easier if you have already setup everything else.
With flag on the item you don't even need to do anything else, you just check if it is active or not and MobX will do everything else. Only 2 items will rerender when you change the flag. The action in your store could look like that:
playItem(itemToPlay) {
this.items.find(item => item.isPlaying)?.isPlaying = false
itemToPlay.isPlaying = true
}

Data from Redux store coming late so it doesn't fill AG grid table/ react hook

I try to get state from redux store and then fill the AG grid table by updateData.
Here is my component.
const DisplayRules = () => {
const rules = useSelector(({rules}) => rules.data);
const dispatch = useDispatch();
const [gridApi, setGridApi] = useState(null);
const [gridColumnApi, setGridColumnApi] = useState(null);
const [rowData, setRowData] = useState(null);
const onGridReady = (params) => {
setGridApi(params.api);
setGridColumnApi(params.columnApi);
const updateData = (data) => {
setRowData(data);
};
if (rules) {
updateData(rules)};
};
function isFirstColumn(params) {
var displayedColumns = params.columnApi.getAllDisplayedColumns();
var thisIsFirstColumn = displayedColumns[0] === params.column;
return thisIsFirstColumn;
}
return (
<div>
<Typography
style={{marginBottom: '5px'}}
component='h5'
variant='h5'
align='center'>
Rule List
</Typography>
<div className='ag-theme-material' style={{height: '450px'}}>
<AgGridReact
frameworkComponents={{
EditSection,
}}
defaultColDef={{
flex: 1,
minWidth: 50,
resizable: true,
headerCheckboxSelection: isFirstColumn,
checkboxSelection: isFirstColumn,
}}
suppressRowClickSelection={true}
rowSelection={'multiple'}
onGridReady={onGridReady}
rowData={rowData}
rowDragManaged={true}
animateRows={true}
overlayNoRowsTemplate='There is no rule added yet'
overlayLoadingTemplate='Loading..'
>
<AgGridColumn headerName= "Name" field="name" rowDrag={true} />
<AgGridColumn headerName= "Type" field="type"/>
<AgGridColumn headerName= "Id" field="id" hide= {true} suppressToolPanel= {true}/>
<AgGridColumn headerName= "Edit" field="Edit" cellRenderer= 'EditSection' cellRendererParams= {{
"gridApi": gridApi}}/>
</AgGridReact>
</div>
<Button
variant='contained'
color='primary'
startIcon={<PreviewIcon />}
onClick={() => {
console.log('previewRules');
}}>
Preview
</Button>
</div>
);
};
Now I get the rules from the store however it's empty at first. Then I get it after a second however rows don't update after it. I tried to use useEffect and set the state (rerender component) every time the rules change but it didn't work. How to solve this problem?
Also, I want it to rerender the component every time Edit Section changed. I tried to setRowData pass as a parameter but it didn't work either. I appreciate any help on this one as well. Thanks
Try with:
React.useEffect(() => {
setRowData(rules);
}, [rules]);
will rerender everytime rules changes. It should work on edit component as well
You can just do
rowData={rules}
and ag-grid will follow the redux state. There is really no need to use local component state in-between.

How to make antd sidebar menu items active depending on the current url?

I am using antd Menu and Menu Item components. My application is such that if you click on a particular menu item the url changes and that menu item is selected. However if you are on dashboard, you can also click on a button that changes the url to the same one as clicked on that particular menu item but in that case the menu item does not get selected. How to solve this issue:
<Menu
onClick={this.handleClick}
mode="vertical"
>
<Link to="/dashboard">
<Menu.Item key="mail" icon={<MailOutlined />}>
Dashboard
</Menu.Item>
</Link>
<Link to="/add-recipe">
<Menu.Item key="app" disabled icon={<AppstoreOutlined />}>
Add Recipes
</Menu.Item>
</Link>
</Menu>
Now in the dashboard component there is also a button that allows user to directly add recipe from there and changes url upon click but the Add Recipe menu item does not selected becuase it is not manually clicked. How to make it active depending on url?
I encountered the same problem and I had to use the props selectedKeys in Menu and use hooks and state to set the current item selected.
To make this works, keys must have the same value as the link.
Example :
function Navigation() {
let location = useLocation();
const [current, setCurrent] = useState(
location.pathname === "/" || location.pathname === ""
? "/dashboard"
: location.pathname,
);
//or simply use const [current, setCurrent] = useState(location.pathname)
useEffect(() => {
if (location) {
if( current !== location.pathname ) {
setCurrent(location.pathname);
}
}
}, [location, current]);
function handleClick(e: any) {
setCurrent(e.key);
}
return (
<Menu
onClick={handleClick}
mode="vertical"
selectedKeys={[current]}
>
<Link to="/dashboard">
<Menu.Item key="/dashboard" icon={<MailOutlined />}>
Dashboard
</Menu.Item>
</Link>
<Link to="/add-recipe">
<Menu.Item key="/add-recipe" disabled icon={<AppstoreOutlined />}>
Add Recipes
</Menu.Item>
</Link>
</Menu>
);
}
export default Navigation;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
'& .MuiPaper-root': {
backgroundColor: '#ffffff',
},
},
active: {
color: theme.palette.primary.main,
'& .MuiTypography-body1': {
fontWeight: 600,
},
// backgroundColor: 'green'
'& .MuiListItemIcon-root': {
color: theme.palette.primary.main,
},
},
}),
);
const [activeLink, setActiveLink] = useState('');
useEffect(() => {
setActiveLink(window.location.pathname);
}, []);
Now in your Menu.item you can apply an active class.
<Menu.Item
className={{path-from-fetched-from-onClick)} === activeLink ? `${classes.active}` : ''}>
...
</Menu.Item>
It might be the case that you've to apply this on your Link, instead of Menu item. But I'm sure it'll will solve the problem.

Change Component property through onClick

I have a ButtonGroup with a few Buttons in it, and when one of the buttons gets clicked, I want to change its color, I kinda want to make them behave like radio buttons:
<ButtonGroup>
<Button
variant={"info"}
onClick={(e) => {
..otherFunctions..
handleClick(e);
}}
>
<img src={square} alt={".."} />
</Button>
</ButtonGroup>
function handleClick(e) {
console.log(e.variant);
}
But that doesnt work, e.variant is undefined.
If it was just a single button I would have used useState and I would be able to make this work, but how do I make it work when there are multiple buttons, how do I know which button is clicked and change the variant prop of that button? And then revert the other buttons to variant="info"
Another approach that I could think of is to create my own Button that wraps the bootstrap Button and that way I can have access to the inner state and use onClick inside to control each buttons state, but I'm not sure if that will work, as then how would I restore the other buttons that werent clicked..?
To further from my comment above, you could create your own button component to handle its own state and remove the need to have lots of state variables in your main component e.g.
const ColourButton = ({ children }) => {
const [colour, setColour] = React.useState(true)
return (
<button
onClick={ () => setColour(!colour) }
style = {{color: colour ? "red" : "blue"} }
>
{ children }
</button>
)
}
That way you can just wrap your image in your new ColourButton:
<ColourButton><img src={square} alt={".."} /></ColourButton>
Edit:
I actually like to use styled-components and pass a prop to them rather than change the style prop directly. e.g. https://styled-components.com/docs/basics#adapting-based-on-props
EDIT: Kitson response is a good way to handle your buttons state locally :)
I like to handle the generation of multiple elements with a function. It allows me to customize handleClick.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [buttons, setButtons] = useState([
{
id: 1,
variant: "info"
},
{
id: 2,
variant: "alert"
}
]);
const handleClick = id => {
setButtons(previous_buttons => {
return previous_buttons.map(b => {
if (b.id !== id) return b;
return {
id,
variant: "other color"
};
});
});
};
const generateButtons = () => {
return buttons.map(button => {
return (
<button key={button.id} onClick={() => handleClick(button.id)}>
Hey {button.id} - {button.variant}
</button>
);
});
};
return <div>{generateButtons()}</div>;
}
https://jrjvv.csb.app/
You can maintain a state variable for your selected button.
export default class ButtonGroup extends Component {
constructor(props) {
super(props);
this.state = {
selected: null
};
}
handleClick = e => {
this.setState({
selected: e.target.name
});
};
render() {
const selected = this.state.selected;
return (
<>
<button
name="1"
style={{ backgroundColor: selected == 1 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="2"
style={{ backgroundColor: selected == 2 ? "red" : "blue" }}
onClick={this.handleClick}
/>
<button
name="1"
style={{ backgroundColor: selected == 3 ? "red" : "blue" }}
onClick={this.handleClick}
/>
</>
);
}
}
Here is a working demo:
https://codesandbox.io/live/OXm3G

Resources