Switch SavePost
to a new url with the slow API.
const SavePost = (post) =>
Http({
url: "http://hyperapp-api.herokuapp.com/slow-api/post",
...
});
When you test the app SavePost
should take about 3 seconds before a notification arrives.
While waiting for the response, you can send more requests without getting confirmation that the previous ones succeeded.
Confused users may try to add the same message again thinking that something went wrong. Disable the Add Post button while the post is saving.
Enhance initial state with the isSaving
property.
const state = {
currentPostText: "",
posts: [],
liveUpdate: true,
isSaving: false
};
Map the property to the button disable
property:
<button onclick=${AddPost} disabled=${state.isSaving}>Add Post</button>
The button reflects the current state of the saving operation.
AddPost
action disables the button:
const AddPost = (state) => {
...
const newState = {
...state,
currentPostText: "",
isSaving: true,
};
...
};
SavePost
action enables the button after a successful response.
You need a new PostSaved
action to be called after the post is saved:
const PostSaved = (state) => ({ ...state, isSaving: false });
const SavePost = (post) =>
Http({
...
action: PostSaved,
});
Test if your version of the button works as intended.
Switch SavePost
to a new url with API returning error responses.
const SavePost = (post) =>
Http({
url: "http://hyperapp-api.herokuapp.com/error-api/post",
...
});
The new API is not only slow but also returns 500 errors.
Enhance initial state with the error
property:
const state = {
currentPostText: "",
posts: [],
liveUpdate: true,
isSaving: false,
error: ""
};
Eventually, you will populate this field with an error value.
Expose the error
in the UI:
<div>${state.error}</div>
You should put the error just above the Add Post button and the input field.
Add PostError
action that will be triggered on HTTP errors.
const PostSaved = state => ({
...state,
isSaving: false
});
const PostError = state => ({
...state,
isSaving: false,
error: "Post cannot be saved."
});
const SavePost = (post) =>
Http({
...
action: PostSaved,
error: PostError
});
PostError
should enable the Add Post button and set the error message.
Add error handling in your fetchEffect
:
const fetchEffect = (dispatch, data) => {
return window
.fetch(data.url, data.options)
.then((response) => (response.ok ? response : Promise.reject(response.json())))
.then((response) => response.json())
.then((json) => {
return dispatch(data.action, json);
})
.catch((e) => dispatch(data.error, e));
};
window.fetch
API doesn't reject a promise by default. We have to inspect the response
object and trigger promise rejection
ourselves.
Test your application.
You should see an error after the post submission.
However, even if the post succeeded the second time, the error won't disappear.
We'd expect the error to disappear at some point, for example when you start typing a new message.
You can change SetPost
action to remove the error message, but there is a better way to model our state.
Take a look at the last two fields you added to the state:
const state = {
...
isSaving: false,
error: ""
};
Those 2 states have 4 possible combinations:
isSaving: false
anderror: ""
(request is idle, user is typing a new message)isSaving: false
anderror: "Post cannot be saved."
(request error after form submission)isSaving: true
anderror: ""
(request is pending)isSaving: true
anderror: "Post cannot be saved."
(should be impossible)
The last combination should be impossible. But the way we modeled our state makes it possible. Of course, you can write some tests to verify the combination never occurs. But you can also model your state to make the impossible state impossible.
The request status can be in 1 of 3 valid states:
{status: "idle"}
{status: "pending"}
{status: "error", message: "Post cannot be saved."}
Introduce 3 valid states we defined in the modeling exercise and set the idle
status as the initial one.
const idle = { status: "idle" };
const saving = { status: "saving" };
const error = {
status: "error",
message: "Post cannot be saved.",
};
const state = {
currentPostText: "",
posts: [],
liveUpdate: true,
requestStatus: idle,
};
A common strategy to scale a growing state object is to split it into smaller objects and combine them.
Find all the places where you were setting isSaving
and error
properties and replace them with requestStatus
.
AddPost
sets the status to saving
:
const AddPost = (state) => {
...
const newState = {
...state,
currentPostText: "",
requestStatus: saving
};
...
};
PostSaved
sets the status to idle
:
const PostSaved = (state) => ({ ...state, requestStatus: idle });
PostError
sets the status to error
:
const PostError = (state) => ({ ...state, requestStatus: error });
Map request error message in the new errorMessage
view fragment:
const errorMessage = ({ message }) => html`<div>${message}</div>`;
Map request status to the button disabled
status in the new addPostButton
view fragment:
const addPostButton = ({ status }) => html`
<button onclick=${AddPost} disabled=${status === "saving"}>Add Post</button>
`;
Delete those two lines:
<div>${state.error}</div>
<button onclick=${AddPost} disabled=${state.isSaving}>Add Post</button>
And replace them with your new view fragments:
${errorMessage(state.requestStatus)}
${addPostButton(state.requestStatus)}
A common strategy to scale growing view functions is to split them into smaller view fragments and delegate to them.
Modify the UpdatePostText
action to remove the error message when a user starts typing a new post.
Solution
const UpdatePostText = (state, currentPostText) => ({
...state,
currentPostText,
requestStatus: idle
});
Now revert your API url
in SavePost
to "http://hyperapp-api.herokuapp.com/api/post"