PAYATHON 2026

如何让 Express server 正确配合 react-admin getOne()?

数据小许

结论

问题不在于 Express 响应太快,也不需要人为添加延迟,而是 create 接口返回的数据结构不符合 react-admin 的 Data Provider 契约。

创建成功后,react-admin 需要收到:

{
  data: {
    id: 123,
    // 其他字段
  }
}

当前服务端很可能直接返回了 res.insertId,也就是一个数字。因此,Data Provider 读取 json.id 时得到的是 undefined:

id: json.id

随后执行 redirect="show" 时,由于没有可用的记录 ID,客户端最终会发出类似这样的请求:

GET /vets/undefined

解决办法是让 Express 的创建接口返回一个包含 id 的 JSON 对象,同时确保 getOne 返回单条对象,而不是数组。

为什么 async/await 没有解决问题

下面的代码并没有真正等待查询完成:

await sql.query("INSERT INTO vets set ?", newVet, function(err, res) {
    // ...
});

await 只能等待 Promise。以回调方式调用 sql.query() 时,如果数据库驱动没有返回代表查询结果的 Promise,这里的 await 基本不起作用。

但这不是 ID 丢失的直接原因。只要 Express 在数据库回调中调用 res.json(),HTTP 响应仍会等到插入完成后再发送。这里的问题在于响应内容的结构,而不是发送响应的时机。

修正创建接口的响应

Model 层可以继续通过回调返回 insertId,但不需要声明为 async:

Vet.createVet = function (newVet, result) {
    sql.query("INSERT INTO vets SET ?", newVet, function (err, res) {
        if (err) {
            console.error("createVet error:", err);
            return result(err, null);
        }

        result(null, res.insertId);
    });
};

然后在 Express controller 中将 ID 包装成对象:

exports.create = function (req, res) {
    Vet.createVet(req.body, function (err, insertId) {
        if (err) {
            return res.status(500).json({
                error: err.message
            });
        }

        return res.status(201).json({
            id: insertId
        });
    });
};

客户端收到的 JSON 将是:

{
  "id": 123
}

这样,现有 Data Provider 的 create 实现就能正确读取 json.id:

create: (resource, params) =>
    httpClient(`${apiUrl}/${resource}`, {
        method: 'POST',
        body: JSON.stringify(params.data),
    }).then(({ json }) => ({
        data: {
            ...params.data,
            id: json.id,
        },
    })),

还可以检查响应中是否包含 ID,避免接口格式错误时继续跳转:

create: async (resource, params) => {
    const { json } = await httpClient(`${apiUrl}/${resource}`, {
        method: 'POST',
        body: JSON.stringify(params.data),
    });

    if (json.id === undefined || json.id === null) {
        throw new Error('Create response does not contain an id');
    }

    return {
        data: {
            ...params.data,
            id: json.id,
        },
    };
},

使用 .then() 或 async/await 都可以,功能上没有区别。最终返回的 Promise 必须解析为一个包含 data.id 的对象。

如果服务端当前直接返回数字

假设 Express 当前执行的是:

res.json(insertId);

客户端收到的 json 本身就是 ID,例如:

123

这种情况下,可以暂时修改 Data Provider:

create: (resource, params) =>
    httpClient(`${apiUrl}/${resource}`, {
        method: 'POST',
        body: JSON.stringify(params.data),
    }).then(({ json }) => ({
        data: {
            ...params.data,
            id: json,
        },
    })),

更稳妥的做法仍然是修改服务端,让它返回:

{
  "id": 123
}

对象形式更便于以后扩展,也不容易与其他响应格式混淆。

修正 getOne 的返回值

MySQL 查询即使只匹配到一条记录,通常也会返回数组:

[
  {
    "id": 123,
    "FirstName": "Adam"
  }
]

而 react-admin 要求 getOne 中的 data 是单个对象:

{
  data: {
    id: 123,
    FirstName: "Adam"
  }
}

因此,服务端最好直接提取第一条记录:

Vet.getVetById = function (vetId, result) {
    sql.query(
        "SELECT * FROM vets WHERE id = ?",
        [vetId],
        function (err, rows) {
            if (err) {
                console.error("getVetById error:", err);
                return result(err, null);
            }

            result(null, rows[0] || null);
        }
    );
};

Controller 还需要处理记录不存在的情况:

exports.findOne = function (req, res) {
    Vet.getVetById(req.params.id, function (err, vet) {
        if (err) {
            return res.status(500).json({
                error: err.message
            });
        }

        if (!vet) {
            return res.status(404).json({
                error: 'Vet not found'
            });
        }

        return res.json(vet);
    });
};

这样,原来的 getOne 可以保持不变:

getOne: (resource, params) =>
    httpClient(`${apiUrl}/${resource}/${params.id}`)
        .then(({ json }) => ({
            data: json,
        })),

如果暂时不能修改服务端,也可以在 Data Provider 中提取数组的第一项:

getOne: (resource, params) =>
    httpClient(`${apiUrl}/${resource}/${params.id}`)
        .then(({ json }) => ({
            data: Array.isArray(json) ? json[0] : json,
        })),

不过,后端接口最好采用统一规则:单条查询返回对象,列表查询返回数组。

推荐的完整响应流程

客户端发出创建请求:

POST /vets

服务端响应:

{
  "id": 123
}

Data Provider 返回给 react-admin:

{
    data: {
        ...params.data,
        id: 123
    }
}

随后,react-admin 使用该 ID 跳转并发出请求:

GET /vets/123

服务端返回单条记录:

{
  "id": 123,
  "FirstName": "Adam",
  "Surname": "Smith"
}

还需要注意的细节

字段名大小写必须一致

创建表单使用的是:

<SelectInput source="sex" />

编辑表单使用的是:

<TextInput source="Sex" />

JavaScript 属性名区分大小写。应根据数据库和 API 的实际字段名,将两处统一为 sex 或 Sex。否则,即使记录成功加载,该字段也可能显示为空。

CORS 响应头不能通过请求头设置

下面的代码只是添加了一个名为 X-Custom-Header 的请求头,并没有配置 CORS:

options.headers.set(
    'X-Custom-Header',
    'Access-Control-Allow-Headers',
    'Access-Control-Allow-Origin',
    '*',
);

Headers.set() 只接收两个参数,后面的参数不会产生预期效果。Access-Control-Allow-Origin 等 CORS 响应头应由 Express 设置,例如使用 cors middleware。这个问题与 ID 丢失没有直接关系,但当前写法容易让人误以为 CORS 已经配置完成。

redirect="show" 需要对应的 Show 页面

redirect="show" 会在创建成功后跳转到该记录的 Show 路由。如果实际需要跳转到编辑页面,应使用:

<TabbedForm redirect="edit">

无论跳转到 show 还是 edit,创建结果中都必须有有效的 data.id。

因此,不需要等待几秒,也不应使用 setTimeout。只需统一接口契约:创建接口返回 { id: insertId },Data Provider 返回 { data: { ..., id } },getOne 则返回 { data: 单条对象 }。

备注:内容仅供参考。