Redux: Updating the State with the Fetched Data

Dan Abramov
InstructorDan Abramov
Share this video with your friends

Social Share Links

Send Tweet
Published 8 years ago
Updated 5 years ago

We will learn how moving the source of truth for the data to the server changes the state shape and the reducers in our app.

[00:00] In the current implementation, we keep all these in memory at once. We have an array of all IDs ever and we get the array of to-dos that we can filter according to the filter passed from React router.

[00:18] However, this only works correctly if all the data from the server is already available in the client, which is usually not the case with applications that fetch something. If we have thousands of to dos on the server, it would be impractical to fetch them all and filter them on the client.

[00:37] Rather than keep on big list of IDs, we'll keep a list of IDs for every tab so that they can be stored separately and filled according with the actions with the fetch data on the corresponding tab.

[00:51] I'm removing the get alt to-do selector because we won't have access to all to dos. We also don't need to filter on the client anymore because we will use the list of to-dos provided by the server.

[01:05] Rather than read from state all IDs, I will read the IDs from state IDs by filter. Then I can use the same technique as before and map the IDs to state by ID look up table to get the actual to-dos.

[01:26] My selector now expects by IDs and IDs by filter to be part of the combined state of the to-dos reducer. The to-dos reducer used to combine the look of table and a list of all IDs which I'm replacing with the list of IDs by filter. IDs by filter is a new combined reducer that I'm creating.

[01:50] It combines a separate list of IDs for every filter. So it's all IDs for the all filter, active IDs for active filter, and completed IDs for the completed filter.

[02:03] The existing all IDs reducer manages an array of IDs, but it handles at to-do action which we don't want to handle anymore, at least not yet, because for now we want to teach it to respond to the data fetched from the server.

[02:20] To handle the receive to-dos action, we want to return a new array of to-dos that we get from the server response. I map the response which is an array of to-dos to a function that just selects an ID from the to-do. We decided to keep all IDs separate from active IDs and completed IDs, so they are fetched completely independently.

[02:45] I'm adding a new reducer called active IDs that also keeps track of an array of IDs, but for to-dos on the active tab. If we think about which actions it needs to handle, we'll realize that it also needs to handle received to-dos action in exactly the same way as all the IDs before it. Both active IDs and all IDs need to return a new state when the received to-dos action fires.

[03:13] However, we need to have a way of telling which ID array we should update.

[03:21] If you recall the receive to-dos action, you might remember that we passed the filter as part of the action. This lets me compare the filter inside the action with a filter that corresponds to the reducer. The all IDs reducer is only interested in the actions with the all filter.

[03:42] Similarly, the active IDs reducer only wants to replace the state when the filter in the action is equal to active. Finally, I can copy and paste my active IDs reducer to get a stub for my completed IDs reducer. I'll just place the word active with the word completed.

[04:04] Now that I wrote all the reducers managing the IDs, I need to update the by ID reducer to actually handle the new to-dos from the response. I'm removing the existing cases because the data does not live locally anymore. Instead, I'll handle the received to-do action just like I do in the other reducers.

[04:25] I'm creating a shallow copy of the state object which corresponds to the lookup table. For every to do object in the response, I want to take it and put it into the next version of the lookup table. I'm replacing whatever is in the next state by to-do ID with the new to-do I just fetched.

[04:46] Finally, I can return the next version of the lookup table from my reducer. Normally the assignment operation is a mutation. However it's fine because next state is a shallow copy, and we're only assigning one level deep. So we're not modifying any of the original state objects. The function itself stays pure.

[05:10] As the last step, I can remove the existing to-do reducer because the logic for adding and toggling to-dos will be implemented as API calls to the server in the future lessons.

[05:23] Let's run the app and inspect how the state changes. Before the action, the original state object contains the to-dos object with an empty lookup table and empty arrays of IDs for every filter. The action object contains the filter and the server response corresponding to this filter with to-do objects inside of it.

[05:49] After handling the action, the to-dos object contains the updated lookup table that has every to-do by its ID, as well as an updated list of IDs by filter where we only have fetched the old to-dos so far, so we have the IDs for all to-dos, but the other filters are amped in.

[06:13] If I change the tab now, it will make another API request and store these IDs separately from the all IDs. Inside the action object, the filter is active now. The response contains only one active to-do.

[06:30] Inside the next state object, the to-dos by ID lookup table is essentially the same because we have not received any new to-dos. However, the IDs by filter object now contains an array of IDs for all filter and a separate array of IDs for the active filter.

[06:49] We have filter and logic to the server, so switching a tab for the first time will take some time for the answers to load. However the next switch is filled instantaneous because we have cached the array of the fetched IDs. Even though we refresh them by fetching them again, the UI can already render the previous version of the array.

[07:11] Let's recap how we updated this state in response to the action with the fetch data. The state shape of the by ID reducer stayed the same. It's still a mapping of to-do IDs to the to-do objects. However, now it handles the received to-dos action when they are fetched from the server.

[07:31] It creates a shallow copy of the current mapping so that it can efficiently reassign its fellows inside a loop. It is a shallow copy. The assignment is only one level deep. So while this is limitation, it does not modify any of the original state objects. So the reducer function stays pure.

[07:53] The action response field is an array of to-dos fetched from the server, so they get merged into the lookup table managed by by ID.

[08:03] Next, I created a separate reducer for every collection of IDs. They all handle the received to-dos action, but to make sure that they handle only the IDs they're interested in, we check the filter.

[08:17] The action response field is an array of to-dos, so we map them to their IDs. The reducers handling to-do IDs handle received to-dos action in the same way but check for different filters. We have the all IDs for the all filter, the active IDs reducer for the active filter, and the completed IDs reducer for the completed filter.

[08:41] Finally, I'm using combine reducers to combine all of them into single IDs by filter reducer. I use the filter values as the keys so that the corresponding state appears under those keys.

[08:59] For example, to read the IDs corresponding to all filter, I can read state IDs by filter and pass all as a dynamic key. In order for IDs by filter to be in the state, I combine the to-dos reducer from by ID and IDs by filter combined reducer.

[09:20] This last my get visible to-do selector read the IDs for the corresponding filter and map them to the lookup table to get an array of to-do objects that we can return to the components.

Jiaming
Jiaming
~ 8 years ago

Now I can understand why we persist list of data as Object in previous lectures :)

Deoward
Deoward
~ 8 years ago

I don't fully understand the reasoning behind fetching the id's by filter vs. fetching all id's and then filtering on the client. For one thing the app is doubling the amount of data it needs to load since All = Active + Completed todos. Furthermore filtering on the client is faster and more responsive. This is just my way of reasoning about the problem and I could be missing something, could you perhaps shine some light on this particular implementation and possible benefits?

Dor
Dor
~ 8 years ago

byId reducer can be implemented with reduce:

const byId = (state = {}, action)=> {
    switch (action.type) {
        case "SET_TODOS":
            return action.todos.reduce((prev, current)=> {
                prev[current.id] = current;
                return prev;
            }, state);
        default:
            return state;
    }

};
~~~
J. Matthew
J. Matthew
~ 4 years ago

I don't fully understand the reasoning behind fetching the id's by filter vs. fetching all id's and then filtering on the client. For one thing the app is doubling the amount of data it needs to load since All = Active + Completed todos. Furthermore filtering on the client is faster and more responsive. This is just my way of reasoning about the problem and I could be missing something, could you perhaps shine some light on this particular implementation and possible benefits?

I wondered about this, too. I think continuing to have an "all" filter undermines and confuses the intention of the lesson a bit.

The reason Dan gives for filtering on the server, copied from the transcript, is as follows: "If we have thousands of todos on the server, it would be impractical to fetch them all and filter them on the client." That directly answers the question. Presumably when dealing with that magnitude of data you wouldn't have an option to view all the todos at once. Even if there were an "all" option it would probably involve pagination, which is itself a kind of filter.

But we only have three todos, and we do have a legacy option to view them all at once, so we might as well just continue fetching them all and filtering client-side. In order to illustrate the point better I think it would have been clearer to remove the "all" filter entirely. Then it would no longer be possible to figure out the contents of one array from the contents of another, because there wouldn't be overlap, and this implementation would make more sense within the context of our tiny dataset.

J. Matthew
J. Matthew
~ 4 years ago

I've been trying to figure out why we bother having separate arrays for each filter type at all, if we're just going to fetch again whenever the filter changes. We could just as well have a single array of ids, which just reflects whatever filter was passed most recently to the server. I have to assume that the reason is to take advantage of caching, as Dan demonstrated, so that if you revisit a filter you instantly see what the app already loaded for that filter.

J. Matthew
J. Matthew
~ 4 years ago

byId reducer can be implemented with reduce:

True, but that implementation mutates state by using it as the initial value for reduce and then changing its properties directly. You could start with { ...state } instead like Dan does. However, mutation (even to the shallow degree that Dan's implementation uses it) can be avoided entirely by using reduce in this way instead:

const byId = (state = {}, action) => {
  switch (action.type) {
    case 'RECEIVE_TODOS':
      return action.response.reduce((currentState, todo) => ({
        ...currentState,
        [todo.id]: todo
      }), state);
    default:
      return state;
  }
};

Even though this implementation starts with state and not a copy, it doesn't matter because each iteration within reduce returns a new state object built by destructuring the previous one and then appending the updated data.

Markdown supported.
Become a member to join the discussionEnroll Today