javascriptreactjsreact-router

How to pass a function to state in react router v6


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>&nbsp; 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>


Solution

  • 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>
                  &nbsp; 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>
      )
    }