如何在 Post 完成 ketting 时收到通知

How to get notified when a Post is done with ketting

我实际上是在用 fastify 的 API 来尝试 react-ketting 的力量。

从挂钩 useResource 中,我可以获得一个提交函数,该函数将生成一个 POST,但我不知道该提交何时完成,而且我无法获得 API 的回复这个POST.

React 不允许这样做?我错过了一个参数还是我必须为 PUT/POST 使用其他东西,比如 axios 才能拥有所有控制权?

// API - with Fastify

// server.js
// Require the framework and instantiate it
const fastify = require('fastify')({
  logger: true
})

fastify.register(require('fastify-cors'), {
  origin: true,
  allowedHeaders: [
    'Content-Type',
    'User-Agent',
    'Authorization',
    'Accept',
    'Prefer',
    'Link'
  ],
  methods: [
    'DELETE',
    'GET',
    'PATCH',
    'POST',
    'PUT',
    'HEAD'
  ],
  exposedHeaders: [
    'Location',
    'Link'
  ]
});

// Loading routes
fastify.register(require('./routes/Products'));

// Run the server!
fastify.listen(5000, '0.0.0.0', function (err, address) {
  if (err)
  {
    fastify.log.error(err)
    process.exit(1)
  }
  fastify.log.info(`server listening on ${address}`)
});

// ./routes/Products.js
async function routes(fastify, options) {

  // Root
  fastify.get('/', async (request, reply) => {
    // Relations
    resource
      .addLink('collection', { href: `/products`, title: 'Products' });

    reply.send(resource);
  });

  // List all products
  fastify.get('/products', async (request, reply) => {
    const resource = halson();

    // Relations
    resource
      .addLink('self', { href: '/products', title: 'Products' })

    // Collection items
    const products = DB.getProducts(); // [{id: 'xxx', name: 'yyy'}, ...]

    if (products && products.length) {
      products.forEach(product => {
        resource.addLink('item', { href: `/products/${product.id}`, title: product.name });
      });
    }

    // Actions like
    resource.addLink('create', { href: '/products/add', title: 'Add product' })

    reply.send(resource);
  })

  // Get product
  fastify.get('/products/:productId', async (request, reply) => {
    const productId = request.params.productId;
    const product = DB.getProductById(productId); // { id: 'xxx', name: 'yyy', ... }
    const resource = halson(product);

    // Relations
    resource
      .addLink('self', { href: `/products/${productId}`, title: `${product.name}` })

    reply.send(resource);
  });

  // Get add product form
  fastify.get('/products/add', async (request, reply) => {
    const resource = halson();

    // Relations
    resource
      .addLink('create-form', { href: 'addProductForm' })

    // Embeded resource
    const initialData = {
      productId: uuidv4(),
      name: ''
    };

    const embed = halson({
      submitData: {
        resource: '/products/add',
        mode: 'POST',
        initialData: initialData,
      },
      jsonSchema: {
        type: 'object',
        title: 'Add a product',
        required: ['name'],
        properties: {
          productId: {
            type: 'string',
            title: 'Product ID'
          },
          name: {
            type: 'string',
            title: 'Product name'
          }
        }
      },
      uiSchema: {
        productId: {
          "ui:readonly": true
        },
        name: {
          "ui:autofocus": true
        }
      },
      formData: initialData
    });

    embed.addLink('self', { href: 'addProductForm' })

    resource.addEmbed('create-form', embed);

    reply.send(resource);
  });

  // Post new product
  fastify.post('/products/add', async (request, reply) => {
    const product = DB.addProduct(request.body); // { id: 'xxx', name: 'yyy', ... }

    reply
      .code(201)
      .header('Location', `/products/${product.id}`)
      .send(halson());
  });

}



// Front

// index.jsx
import { Client } from 'ketting';
import { KettingProvider } from 'react-ketting';

const BASE_URL = 'http://localhost:5000';
const KettingClient = new Client(BASE_URL);

// HOC useResource
const withUseResource = WrappedComponent => ({ resource, ...props }) => {
  const {
    loading,
    error,
    data,
    setData,
    submit,
    resourceState,
    setResourceState,
  } = useResource(resource);

  if (loading) return 'Loading ...';
  if (error) return `Error : ${error.message}`;

  const { children } = props;

  const newProps = {
    resource,
    resourceData: data,
    setResourceData: setData,
    submitResourceData: submit,
    resourceState,
    setResourceState,
  };

  return (
    <WrappedComponent {...newProps} {...props}>
      {children}
    </WrappedComponent>
  );
};

// HOC useCollection
const withUseCollection = WrappedComponent => ({ resource, ...props }) => {
  const [collection, setCollection] = useState([]);
  const { loading, error, items } = useCollection(resource);

  useEffect(() => {
    setCollection(items);

    return () => {
      setCollection([]);
    };
  }, [items]);

  if (loading) return 'Loading ...';
  if (error) return `Error : ${error.message}`;

  const { children } = props;

  const newProps = {
    resource,
    collection,
  };

  return (
    <WrappedComponent {...newProps} {...props}>
      {children}
    </WrappedComponent>
  );
};

// Main Component
function Root() {
  return (
    <KettingProvider client={KettingClient}>
      <Container />
    </KettingProvider>
  );
}


function Container() {
  const [url, setUrl] = useState(`${BASE_URL}/`);
  const [resource, setResource] = useState(null);

  useEffect(() => {
    setResource(KettingClient.go(url));
  }, [url]);

  if (!resource) {
    return null;
  }

  return <Content resource={resource} />;
}

const SimpleContent = ({ resource, resourceState }) => {
  let content = null;

  if (resourceState.links.has('collection')) {
    content = <Catalog resource={resource.follow('collection')} />;
  }

  return content;
};

const Content = withUseResource(SimpleContent);

const SimpleCatalog = ({ resource, resourceState, collection }) => {
  const [addProductBtn, setAddProductBtn] = useState(null);

  useEffect(() => {
    if (resourceState?.links.has('create')) {
      setAddProductBtn(
        <AddNewProduct resource={resource.follow('create')} />
      );
    }
  }, []);

  return (
    <>
      {collection.map((item, index) => (
        <Product key={index} resource={item} />
      ))}
      {addProductBtn}
    </>
  );
};

const Catalog = withUseResource(withUseCollection(SimpleCatalog));

const SimpleProduct = ({ resourceData }) => {
  return <p>{resourceData.name}</p>;
};

const Product = withUseResource(SimpleProduct);

const SimpleNewProduct = ({ resource, resourceState }) => {
  const [openDialog, setOpenDialog] = useState(false);
  const [dialogConfig, setDialogConfig] = useState({});

  useEffect(() => {
    if (resourceState.links.has('create-form')) {
      setDialogConfig({
        title: '',
        content: (
          <div>
            <FormDialog
              resource={resource.follow('create-form')}
              setOpenDialog={setOpenDialog}
            />
          </div>
        ),
        actions: '',
      });
    }

    return () => {
      setDialogConfig({});
    };
  }, []);

  return (
    <>
      <Button onClick={() => setOpenDialog(true)}>+</Button>
      <PopupDialog
        config={dialogConfig}
        open={openDialog}
        onClose={() => setOpenDialog(false)}
      />
    </>
  );
};

const AddNewProduct = withUseResource(SimpleNewProduct);

const SimpleFormDialog = ({ resourceData, setOpenDialog }) => {
  const { jsonSchema, uiSchema, formData, submitData } = resourceData;
  const { loading, error, setData, submit } = useResource(
    submitData
  );

  if (loading) return 'Loading ...';
  if (error) return `Error : ${error.message}`;

  const handleChange = fd => {
    setData(fd);
  };

  const handleSubmit = () => {
    // How to get notified that the post has been processed,
    // so that I can go to the new product page,
    // or have the catalog to be refreshed ?
    submit();
    setOpenDialog(false);
  };

  return (
    <div>
      { /* React JsonSchema Form */ }
      <RjsfForm
        JsonSchema={jsonSchema}
        UiSchema={uiSchema}
        formDataReceived={formData}
        handleChange={handleChange}
        handleSubmit={handleSubmit}
      />
    </div>
  );
};

const FormDialog = withUseResource(SimpleFormDialog);

const PopupDialog = ({ open, onClose, config }) => {
  if (!config) return null;

  const { title, content, actions, props } = config;

  return (
    <Dialog fullWidth maxWidth='md' open={open} onClose={onClose} {...props}>
      <DialogTitle>{title}</DialogTitle>
      <DialogContent>{content}</DialogContent>
      <DialogActions>{actions}</DialogActions>
    </Dialog>
  );
};

有两个主要 reasons/processes 需要与 Ketting 一起完成 POST 请求。

  1. 创建新资源/将新资源添加到 collection。
  2. 执行任意 RPC 调用/提交表单。

正在创建新资源

当你使用useResource()钩子,并使用submit()函数时,这样做的主要目的是处理第一种情况。

因为您是严格制作新资源,所以期望响应包含:

  1. 一个201 Created状态
  2. A Location header 指向新创建的资源。

可选地,您的服务器可以 return 新创建资源的 body,但要做到这一点,服务器还必须包含 Content-Location header。

如果那是您的目的,您只需监听已有组件的状态变化即可获得 POST 请求的结果。

执行 RPC POST 请求/提交表单

如果您属于第二类并且只想执行任意 POST 请求并读取响应,则您应该 而不是 使用 submit()函数 return 从钩子中编辑。

useResource 钩子上的 submit() 函数实际上用作 'resource state submissions' 机制,对于其他任意 POST 请求重载它是不好的。

相反,只需在 Resource 本身上使用 .post() 函数。

const response = await props.resource.post({
  data: body
});