I want to share state between two routes when I click on the link for one of the routes (NewUser
). The state that I want to share and the logic modifying it are both held in the Users
route. I want to pass the logic to change the state to the NewUsers
route.
When I pass a string to the state object in router Link
, I am able to access it in the NewUsers
component. However, I get null
when I pass a function.
I know that I can use context/redux, but I would prefer if I can do it this way.
Users
route:
function Users() {
const [users, setUsers] = useState([]);
return (
<Card sx={{ padding: "2rem", mt: "2rem" }}>
<MDBox
display="flex"
flexDirection="row"
justifyContent="space-between"
>
<MDTypography variant="body2">{`You currently have ${users.length} users`}</MDTypography>
<MDButton variant="gradient" color="info" size="small">
<Link to="/settings/users/new-user" state={setUsers: setUsers}> //this is how I want to pass the state
<MDBox
display="flex"
alignItems="center"
color="white"
fontWeight="normal"
>
<Icon>add</Icon> Add New User
</MDBox>
</Link>
</MDButton>
</MDBox>
</Card>
NewUsers
route:
function NewUser({history}) {
const location = useLocation();
const saveChanges = (e) => {
location.state.setUsers({
fname: values.firstName,
lname: values.lname,
email: values.email,
});
navigate("/settings/users");
};
return(
<MDBox py={3} mb={20} height="62vh">
<Grid
container
justifyContent="center"
alignItems="center"
sx={{ height: "100%", mt: 0 }}
>
<Grid item xs={12} lg={12}>
<Formik
initialValues={initialValues}
validationSchema={currentValidation}
onSubmit={(values) => {
setValues(values);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form id={formId} autoComplete="off">
<Card sx={{ height: "100%", width: "100%" }}>
<MDBox px={3} py={4}>
<MDBox display="flex">
<ButtonWrapper
fullWidth={false}
handleClick={saveChanges}
>
Save Changes
</ButtonWrapper>
</MDBox>
<MDBox>
{getStepsContent({
values,
touched,
formField,
errors,
})}
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
)
}
Routing code:
{
type: "collapse",
name: "Settings",
key: "settings",
icon: <Icon fontSize="small">settings</Icon>,
collapse: [
{
name: "Users",
key: "users",
route: "/settings/users",
// icon: <Icon fontSize="small">users</Icon>,
component: <Users />,
},
{
name: "Companies",
key: "companies",
route: "/settings/companies",
component: <Companies />,
},
{
name: "Billing",
key: "billing",
route: "/settings/billing",
component: <Billing />,
},
{
name: "Integrations",
key: "integrations",
route: "/settings/integrations",
component: <Integrations />,
},
],
},
{
name: "New User",
key: "new user",
route: "/settings/users/new-user",
noCollapse: true,
component: <NewUser />,
},
{
type: "collapse",
name: "Sign Out",
key: "signout",
route: "/sign-out",
icon: <Icon fontSize="small">logout</Icon>,
component: <SignOut />,
noCollapse: true,
},
];
function that renders the routes:
const getRoutes = (allRoutes) =>
allRoutes.map((route) => {
if (route.collapse) {
return getRoutes(route.collapse);
}
if (route.route) {
return <Route exact path={route.route} element={route.component} key={route.key} />;
}
return null;
});
<Routes>
{getRoutes(routes)}
{/* <Route path="*" element={<Navigate to="/dashboard" />} /> */}
<Route path="*" element={<Console />} />
</Routes>
The state value sent via the Link
component needs to be JSON serializable. Javascript functions are not serializable. Instead of trying to pass a function through to a target component I recommend lifting the state up to a common ancestor so the state and callback function is accessible to both components.
I would suggest using a React context to hold the users
state and provide out the state value and an updater function to add a user object. react-router-dom
has a "built-in" way to do this via a layout route component that renders an Outlet
component that wraps nested routes.
Example:
import { Outlet } from 'react-router-dom';
const UsersProvider = () => {
const [users, setUsers] = useState([]);
const addUser = (user) => {
setUsers((users) => users.concat(user));
};
return <Outlet context={{ users, addUser }} />;
};
...
<Routes>
...
<Route path="/settings/users" element={<UsersProvider />}>
<Route index element={<Users />} />
<Route path="new-user" element={<NewUser />} />
</Route>
...
</Routes>
Users
const Users = () => {
const { users } = useOutletContext();
return (
<Card sx={{ padding: "2rem", mt: "2rem" }}>
<Box display="flex" flexDirection="row" justifyContent="space-between">
<Typography variant="body2">
You currently have {users.length} users
</Typography>
<Button variant="gradient" color="info" size="small">
<Link to="/settings/users/new-user">
<Box
display="flex"
alignItems="center"
color="white"
fontWeight="normal"
>
<Icon>add</Icon>
Add New User
</Box>
</Link>
</Button>
</Box>
</Card>
);
};
NewUser
function NewUser({ history }) {
const navigate = useNavigate();
const { addUser } = useOutletContext();
const saveChanges = (e) => {
addUser({
fname: values.firstName,
lname: values.lname,
email: values.email,
});
navigate("/settings/users");
};
return(
<MDBox py={3} mb={20} height="62vh">
<Grid
container
justifyContent="center"
alignItems="center"
sx={{ height: "100%", mt: 0 }}
>
<Grid item xs={12} lg={12}>
<Formik
initialValues={initialValues}
validationSchema={currentValidation}
onSubmit={(values) => {
setValues(values);
}}
>
{({ values, errors, touched, isSubmitting }) => (
<Form id={formId} autoComplete="off">
<Card sx={{ height: "100%", width: "100%" }}>
<MDBox px={3} py={4}>
<MDBox display="flex">
<ButtonWrapper
fullWidth={false}
handleClick={saveChanges}
>
Save Changes
</ButtonWrapper>
</MDBox>
<MDBox>
{getStepsContent({
values,
touched,
formField,
errors,
})}
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
)
}