You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router v6函数传递报错:Uncaught TypeError: foo is not a function

React Router 提交函数传递失败问题

开发React Router项目时,无法将Form组件的订单提交函数传递给App组件,触发Uncaught TypeError: foo is not a function错误。尝试使用useLocation钩子接收App组件的props,但解构的props未被实际使用;同样的状态传递方式在Orders组件中可行,但Form组件无法正常工作。

文件结构

src:
-- App.jsx
-- Forms.jsx
-- Orders.jsx

相关代码

App.jsx

import { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { Button, Typography } from '@mui/material';

import Form from './Form.jsx';
import Orders from './Orders.jsx';
import Error from './Error.jsx';

import './App.css';

const starterOrders = [
    {
        firstName: 'Billy',
        lastName: 'Gibbons',
        orderId: 'ZZTop0000001',
        productName: 'Hat',
        quantity: 2,
        price: 14.99,
    },
    {
        firstName: 'Dusty',
        lastName: 'Hill',
        orderId: 'ZZTop0000002',
        productName: 'Shirt',
        quantity: 1,
        price: 19.99,
    },
    {
        firstName: 'Frank',
        lastName: 'Beard',
        orderId: 'ZZTop0000003',
        productName: 'Hat',
        quantity: 2,
        price: 29.98,
    },
    {
        firstName: 'David',
        lastName: 'Bowie',
        orderId: 'UndrPr3ssr81',
        productName: 'Sweatshirt',
        quantity: 1,
        price: 29.98,
    },
];

const navBackgroundStyles = {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    backgroundColor: 'rgb(255, 255, 255)',
};

function App() {
  const [orders, setOrders] = useState(starterOrders);
  
  useEffect(() => {
    const cachedData = window.localStorage.getItem('ORDERS_ARRAY');
    if (cachedData?.length) {
      setOrders(JSON.parse(cachedData));
    }
  }, [])

    const newOrderFunc = (order) => {
    setOrders((prevOrders) => {
      const newOrders = [order, ...prevOrders]
      window.localStorage.setItem("ORDERS_ARRAY", JSON.stringify(newOrders));
            return newOrders;
        });
    };

    return (
        <Router>
            <div style={navBackgroundStyles}>
                <div>
                    <Typography
                        sx={{
                            marginLeft: '1rem',
                            fontSize: '2rem',
                            color: 'rgb(33, 150, 243)',
                        }}
                    >
                        Clothing Co.
                    </Typography>
                </div>
                <div>
                    <Link to="/" state={{ onSaveOrderData: newOrderFunc }}>
                        <Button sx={{ marginRight: '1rem' }} variant="text">
                            Place Order
                        </Button>
                    </Link>
                    <Link to="/orders" state={{ orders: orders }}>
                        <Button sx={{ marginRight: '1rem' }} variant="text">
                            Orders
                        </Button>
                    </Link>
                </div>
            </div>
            <Routes>
                <Route path="/" element={<Form />} />
                <Route path="/orders" element={<Orders />} />
                <Route path="/*" element={<Error />} />
            </Routes>
        </Router>
    );
}

export default App;

Form.jsx

import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import {
    Alert,
    Box,
    Button,
    InputAdornment,
    InputLabel,
    MenuItem,
    OutlinedInput,
  Select,
  Slide,
    TextField,
} from '@mui/material';

export default function Form({ onSaveOrderData }) {
  const location = useLocation();
  const saveFunction = location.state?.onSaveOrderData;

    const [isActive, setIsActive] = useState(false);
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [product, setProduct] = useState('');
    const [quantity, setQuantity] = useState('');
    const [price, setPrice] = useState('');
    const quantityMenu = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Generate random Order IDs
    const makeId = () => {
        var result = '';
        var characters =
            'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        for (let i = 0; i < 12; i++) {
            result += characters.charAt(
                Math.floor(Math.random() * characters.length)
            );
        }
        return result;
    };

    // Converts product value integer to product name
    const productName = () => {
        switch (product) {
            case 1:
                return 'Shirt';
            case 2:
                return 'Hat';
            case 3:
                return 'Sweatshirt';
        }
    };

    // Calculates order cost on change of product or quantity state
    useEffect(() => {
        if (quantity > 0) {
            switch (product) {
                case 1:
                    setPrice((19.99 * quantity).toFixed(2));
                    break;
                case 2:
                    setPrice((14.99 * quantity).toFixed(2));
                    break;
                case 3:
                    setPrice((29.98 * quantity).toFixed(2));
                    break;
            }
        }
    }, [product, quantity]);

    // Sends new order to App, resets form fields
    const submitHandler = (e) => {
        e.preventDefault();

        const newOrder = {
            firstName: firstName,
            lastName: lastName,
            orderId: makeId(),
            productName: productName(),
            quantity: quantity,
            price: price,
        };

        setIsActive(true);
        saveFunction(newOrder);
        setFirstName('');
        setLastName('');
        setProduct('');
        setQuantity('');
        setPrice('');
    };

    return (
        <div
            style={{
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
        height: '90vh'
            }}
        >
            <div style={{ position: 'fixed', top: '60px', right: '10px' }}>
                <Slide direction="left" in={isActive} mountOnEnter unmountOnExit>
                    <Alert
                        severity="success"
                        onClose={() => setIsActive(false)}
                        style={{ display: isActive ? 'flex' : 'none' }}
                    >
                        Thank you for your order!
                    </Alert>
                </Slide>
            </div>
            <Box
                sx={{
                    padding: '2rem',
                    width: 'fit-content',
                    border: '1px solid rgb(148, 148, 148)',
                    borderRadius: '5px',
                    backgroundColor: 'rgb(255, 255, 255)',
                }}
            >
                <TextField
                    label="First Name"
                    variant="standard"
                    fullWidth
                    sx={{ marginRight: '5px' }}
                    value={firstName}
                    onChange={(e) => setFirstName(e.target.value)}
                />
                <TextField
                    label="Last Name"
                    variant="standard"
                    fullWidth
                    sx={{ marginRight: '5px' }}
                    value={lastName}
                    onChange={(e) => setLastName(e.target.value)}
                />
                <InputLabel
                    style={{
                        display: 'block',
                        margin: '1rem 0 0',
                        color: 'rgb(102, 102, 102)',
                    }}
                >
                    Product
                </InputLabel>
                <Select
                    fullWidth
                    value={product}
                    onChange={(e) => setProduct(e.target.value)}
                >
                    <MenuItem value="">
                        <em>Select product</em>
                    </MenuItem>
                    <MenuItem value={1}>Shirt</MenuItem>
                    <MenuItem value={2}>Hat</MenuItem>
                    <MenuItem value={3}>Sweatshirt</MenuItem>
                </Select>
                <InputLabel
                    style={{
                        display: 'inline-block',
                        margin: '1rem 0 0',
                        color: 'rgb(102, 102, 102)',
                    }}
                >
                    Quantity
                </InputLabel>
                <Select
                    fullWidth
                    value={quantity}
                    onChange={(e) => setQuantity(e.target.value)}
                >
                    <MenuItem value="">
                        <em>Select quantity</em>
                    </MenuItem>
                    {quantityMenu.map((item) => {
                        return (
                            <MenuItem key={item} value={item}>
                                {item}
                            </MenuItem>
                        );
                    })}
                </Select>
                <InputLabel
                    htmlFor="outlined-adornment-amount"
                    sx={{ margin: '1rem 0 0' }}
                >
                    Amount
                </InputLabel>
                <OutlinedInput
                    fullWidth
                    value={price}
                    startAdornment={<InputAdornment position="start">$</InputAdornment>}
                    inputProps={{ readOnly: true }}
                />
                <Button
                    variant="outlined"
                    fullWidth
                    sx={{
                        marginTop: '1.25rem',
                        fontSize: '1rem',
                        color: 'dark',
                    }}
                    onClick={submitHandler}
                >
                    Place Order
                </Button>
            </Box>
        </div>
    );
}

Orders.jsx

import React from 'react';
import { useLocation } from 'react-router-dom';
import {
    Paper,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Typography,
} from '@mui/material';

export default function Orders({orders}) {
    const location = useLocation();
    const ordersList = location.state?.orders;

    return (
        <TableContainer component={Paper}>
            <Typography variant="h1">Orders</Typography>
            <Table sx={{ minWidth: 600 }} aria-label="simple table">
                <TableHead>
                    <TableRow>
                        <TableCell>First Name</TableCell>
                        <TableCell>Last Name</TableCell>
                        <TableCell>Order ID</TableCell>
                        <TableCell>Product Name</TableCell>
                        <TableCell>Quantity</TableCell>
                        <TableCell>Price</TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {ordersList.map((order) => (
                        <TableRow
                            key={order.orderId}
                            sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
                        >
                            <TableCell scope="row">{order.firstName}</TableCell>
                            <TableCell scope="row">{order.lastName}</TableCell>
                            <TableCell>{order.orderId}</TableCell>
                            <TableCell>{order.productName}</TableCell>
                            <TableCell>{order.quantity}</TableCell>
                            <TableCell>{'$' + `${order.price}`}</TableCell>
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
        </TableContainer>
    );
}

问题分析

  1. 函数传递的局限性:通过Link的state传递函数时,函数无法被序列化,若用户直接访问路由(如刷新页面),location.state会丢失,导致saveFunction为undefined,调用时触发错误。
  2. 未正确使用props:Form组件中声明了onSaveOrderData props但未实际使用,而是依赖location.state;若用户直接进入根路径,location.state不会包含该函数,导致调用失败。

解决方案

方案1:直接通过props传递函数(推荐)

这种方式更可靠,不受路由访问方式影响:

  1. 修改App.jsx中的Route,直接传递函数作为props:
<Route path="/" element={<Form onSaveOrderData={newOrderFunc} />} />
  1. 修改Form.jsx,移除useLocation相关代码,直接使用props中的函数:
export default function Form({ onSaveOrderData }) {
    // 移除以下两行
    // const location = useLocation();
    // const saveFunction = location.state?.onSaveOrderData;

    // 在submitHandler中调用props中的函数
    const submitHandler = (e) => {
        e.preventDefault();

        const newOrder = {
            firstName: firstName,
            lastName: lastName,
            orderId: makeId(),
            productName: productName(),
            quantity: quantity,
            price: price,
        };

        setIsActive(true);
        onSaveOrderData(newOrder);
        setFirstName('');
        setLastName('');
        setProduct('');
        setQuantity('');
        setPrice('');
    };
}

方案2:修复useLocation传递逻辑(不推荐)

若坚持使用location.state,需在调用前检查函数是否存在,避免错误:

const submitHandler = (e) => {
    e.preventDefault();

    const newOrder = {
        // ... 订单数据
    };

    setIsActive(true);
    // 先验证函数是否存在
    if (typeof saveFunction === 'function') {
        saveFunction(newOrder);
    } else {
        console.error('请通过页面导航链接进入下单页面');
    }
    // ... 重置表单
};

但此方式仍存在刷新页面丢失函数的问题,仅适用于内存内跳转场景。

内容的提问来源于stack exchange,提问作者Patrick Mahloy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 08:10:28