How to get notified when a Post is done with ketting

327 views Asked by At

I'm actually trying the power of react-ketting with a fastify's API.

From the hook useResource, I can get a submit function that will make a POST but I can't know when that submit is done and I can't get the API reply of this POST.

React doesn't allow this? Did I miss a param or must I use something else for PUT/POST like axios in order to have all control ?

// 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>
  );
};

1

There are 1 answers

1
Evert On BEST ANSWER

There's two main reasons/processes to do POST requests with Ketting.

  1. To create a new resource / add a new resource to a collection.
  2. To do an arbitrary RPC call / submit a form.

Creating a new resource

When you use the useResource() hook, and use the submit() function, the primary purpose of this is to deal with the first case.

Because you are strictly making a new resource, the expectation is that the response contains:

  1. A 201 Created status
  2. A Location header pointing to the newly created resource.

Optionally, you the server can return the body of the newly created resource, but to do that, the server must also include a Content-Location header.

If that was your purpose, you can get the result of the POST request simply by listening to state changes in the component you already have.

Doing a RPC POST request / submit a form

If you are in the second category and just want to do an arbitrary POST request and read the response, you should not use the submit() function returned from the hook.

The submit() function on the useResource hook is really used as a 'resource state submissions' mechanism, and it's not good to overload this for other arbitrary POST requests.

Instead, just use the .post() function on the Resource itself.

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