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>
);
};
There's two main reasons/processes to do POST requests with Ketting.
Creating a new resource
When you use the
useResource()
hook, and use thesubmit()
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:
201 Created
statusLocation
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 theuseResource
hook is really used as a 'resource state submissions' mechanism, and it's not good to overload this for other arbitraryPOST
requests.Instead, just use the
.post()
function on theResource
itself.