TL;DR: Is there some well-known solution out there using React/Redux for being able to offer a snappy and immediately responsive UI, while keeping an API/database up to date with changes that can gracefully handle failed API requests?
I’m looking to implement an application with a “card view” using https://github.com/atlassian/react-beautiful-dnd where a user can drag and drop cards to create groups. As a user creates, modifies, or breaks up groups, I’d like to make sure the API is kept up to date with the user’s actions.
HOWEVER, I don’t want to have to wait for an API response to set the state before updating the UI.
I’ve searched far and wide, but keep coming upon things such as https://redux.js.org/tutorials/fundamentals/part-6-async-logic which suggests that the response from the API should update the state.
For example:
export default function todosReducer(state = initialState, action) { switch (action.type) { case 'todos/todoAdded': { // Return a new todos state array with the new todo item at the end return [...state, action.payload] } // omit other cases default: return state } }
As a general concept, this has always seemed odd to me, since it’s the local application telling the API what needs to change; we obviously already have the data before the server even responds. This may not always be the case, such as creating a new object and wanting the server to dictate a new “unique id” of some sort, but it seems like there might be a way to just “fill in the blanks” once the server does response with any missing data. In the case of an UPDATE vs CREATE, there’s nothing the server is telling us that we don’t already know.
This may work fine for a small and lightweight application, but if I’m looking at API responses in the range of 500-750ms on average, the user experience is going to just be absolute garbage.
It’s simple enough to create two actions, one that will handle updating the state and another to trigger the API call, but what happens if the API returns an error or a network request fails and we need to revert?
I tested how Trello implements this sort of thing by cutting my network connection and creating a new card. It eagerly creates the card immediately upon submission, and then removes the card once it realizes that it cannot update the server. This is the sort of behavior I’m looking for.
I looked into https://redux.js.org/recipes/implementing-undo-history, which offers a way to “rewind” state, but being able to implement this for my purposes would need to assume that subsequent API calls all resolve in the same order that they were called – which obviously may not be the case.
As of now, I’m resigning myself to the fact that I may need to just follow the established limited pattern, and lock the UI until the API request completes, but would love a better option if it exists within the world of React/Redux.
Advertisement
Answer
The approach you’re talking about is called “optimistic” network handling — assuming that the server will receive and accept what the client is doing. This works in cases where you don’t need server-side validation to determine if you can, say, create or update an object. It’s also equally easy to implement using React and Redux.
Normally, with React and Redux, the update flow is as follows:
- The component dispatches an async action creator
- The async action creator runs its side-effect (calling the server), and waits for the response.
- The async action creator, with the result of the side-effect, dispatches an action to call the reducer
- The reducer updates the state, and the component is re-rendered.
Some example code to illustrate (I’m pretending we’re using redux-thunk here):
// ... in my-component.js: export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch(MyActions.UpdateData(someDataFromSomewhere)); }); return (<div />); }; // ... in actions.js export const UpdateData = async (data) => (dispatch, getStore) => { const results = await myApi.postData(data); dispatch(UpdateMyStore(results)); };
However, you can easily flip the order your asynchronous code runs in by simply not waiting for your asynchronous side effect to resolve. In practical terms, this means you don’t wait for your API response. For example:
// ... in my-component.js: export default () => { const dispatch = useDispatch(); useEffect(() => { dispatch(MyActions.UpdateData(someDataFromSomewhere)); }); return (<div />); }; // ... in actions.js export const UpdateData = async (data) => (dispatch, getStore) => { // we're not waiting for the api response anymore, // we just dispatch whatever data we want to our reducer dispatch(UpdateMyStore(data)); myApi.postData(data); };
One last thing though — doing things this way, you will want to put some reconciliation mechanic in place, to make sure the client does know if the server calls fail, and that it retries or notifies the user, etc.