In the beginning of the video, I’m talking about not storing the comment form data in the frontity state, but I thought about this some more and it’s probably not a good idea so you can forget about it for now .
I have a better idea for improving the API (in the future), but I think we shouldn’t focus on it right now. Skip to the very end for a suggested API.
There are other small changes that I want to propose:
-
Store the validation errors returned from WordPress in the state. The REST API returns the validation errors for each of the fields in the comment. E.g. if you send a request with a malformed author_email
, the API returns:
{
"code": "rest_invalid_param",
"message": "Invalid parameter(s): author_email",
"data": {
"status": 400,
"params": {
"author_email": "Invalid email address."
}
}
}
With this info available, I think that we should store the validation errors in the state. The error message from data.params.author_email
should be stored under form.errors
This would look like:
export interface Form {
fields: Fields;
submitted?: Submitted;
errors: CommentErrors; // <--- store the errors here.
}
// It's kind of an awkward type, but it's easier to iterate over all the errors this way.
// if it was an object, we would have to do `Object.entries()` first.
type CommentErrors = {name: keyof Fields, error: string}[]
-
Add the newly submitted comment to state.source.comments
directly. We can do that because the REST API returns the newly created resource after a successful POST request. This is what I get after posting a new comment.
{
"id": 3,
"post": 1,
"parent": 0,
"author": 0,
"author_name": "Michal",
"author_url": "",
"date": "2020-08-28T23:19:15",
"date_gmt": "2020-08-28T23:19:14",
"content": {
"rendered": "<p>hello this is a postman test</p>\n"
},
"link": "http://localhost:8080/hello-world/#comment-3",
"status": "hold",
"type": "comment",
"author_avatar_urls": {
"24": "http://1.gravatar.com/avatar/160475e57835d6b02f6bfadbb05d853c?s=24&d=mm&r=g",
"48": "http://1.gravatar.com/avatar/160475e57835d6b02f6bfadbb05d853c?s=48&d=mm&r=g",
"96": "http://1.gravatar.com/avatar/160475e57835d6b02f6bfadbb05d853c?s=96&d=mm&r=g"
},
"meta": [],
"_links": {
//... cut out for brevity
This would also mean that we would not need to store the comment data on the submitted
property. We can rename this property to status
and use it to store only the submission status.
export interface Form {
fields: Fields;
status?: Status;
errors: CommentErrors;
}
export interface Status {
isPending: boolean;
// isOnHold: boolean; not needed, because we have that in the Response
// isApproved: boolean; not needed either.
isError: boolean;
/**
* The error message that was returned by the REST API
*/
errorMessage: string;
/**
* The error code. Those are defined internally in the WordPress REST API.
* @example rest_comment_invalid_post_id
*/
errorCode: string;
/**
* The HTTP status code that might have been received
* from the WordPress REST API.
*/
errorStatusCode?: number;
// timestamp: number; I think that is also not needed anymore
// id?: number; Not needed anymore either
}
Then, we would also have to actually populate the data in the state using populate()
and insert the new comment in the existing tree of comments:
const populated = await populate({ response, state });
/* This is the part that I'm not sure about.
I think that the comments are sorted ascending by the `id` right?
So, the comments (on the same level) will always be sorted by id?
Something like:
comment 1
comment 4
comment 5
comment 2
comment 3
*/
// Then we can iterate over the data.items to insert the new comment here.
const { id } = await response.json();
const data = state.source.data[route];
-
Provide an updateField()
action.
I think we in addition to the updateFields()
(plural) we should have an action that can update an individual field. This would have better ergonomics for the user and also result in only rerendering one field instead of the the whole form when updating a field value. It could be simply something like:
actions: {
updateField: ({ state }) => (postId, name, value) => {
state.comments.form[postId]?.fields?[name] = value;
}
}
Future API improvement
I was concerned about interoperability with popular form libraries like formik or react-hook-form, which are the idiomatic way to create forms in react. Rolling your own forms in react is a pain in the ass without those .
I was also thinking how to generally improve the developer experience of frontity.
We are going to provide the basic primitives for working with the WP comments which are:
- form state,
-
submit()
action
-
updateFields()
action
-
updateField
()` action
I propose that we also provide a useComments()
hook which should cover 90% of the most basic cases which is just wiring some fields to the state, showing errors and submitting the form.
This way, we can recommend the users to use the hook for most cases and only reach to use the underlying primitives directly if the hook is not enough!
(the ref
-based API is inspired by the react-hook-form )
const useComments = () => {
const { state, actions } = useConnect();
const { id: postId } = state.source.get(state.router.link);
// It's a bit of a hack but it works well!
const register = useCallback((ref) => {
if (ref) {
ref.addEventListener("input", ({ target: { name, value } }) => {
actions.updateField(postId, name, value);
});
}
}, []);
const submit = (e) => {
e.preventDefault();
actions.comments.submit(postId);
};
return { submit, register, errors: state.comments.form.errors };
};
And for an example of how you’d use it:
const CommentComponent = connect(() => {
const { register, submit, errors } = useComments();
return (
<form onSubmit={submit}>
<input name="author_name" ref={register} />
<input name="author_email" ref={register} />
{/* We could show both client-side validation and backend errors here. */}
{errors && errors.map(err =>
<span>{err.name} : {err.errorMessage} </span>
)}
<input type="submit" />
</form>
);
})
However, I think we can work on this in the future because:
- It’s not necessary to use the comments.
- It only works if you attach the ref directly to the DOM element so wont work out of the box with most component libraries like Chakra, Bootstrap, etc. I think we would need to mess with forwardRef to make it work.
- There are probably many other edge cases that I didnt think of yet