Reputation: 13987
Normalizr is great at creating structured JSON repositories of entities.
We have many cases displaying lists of data e.g. posts
that have been normalised. Where posts
are listed the API response is limited to a few key fields.
We also have cases where we display one of these posts
although we now need to fetch the FULL JSON entity from the API with all the fields.
How is it best to deal with this?
A a seperate reducer, thunk/saga, selectors and actions?
B simply insert the extended version of thepost
fetched from the API into the reducer. Reusing the selectors etc from before?
Upvotes: 7
Views: 1798
Reputation: 15908
I agree with both of your two choices and would have come to the same conclusion. But let's have a closer look at them to see an advantage form one over the other:
(B) You can merge the post entities (preview and full representation) as one entity in your reducer, but you would keep track of the result
arrays (preview and full representation), which you would get from the normalizr normalized data after the API requests. Then you can easily distinguish afterwards, if you already have the full representation of the post. Your sub-state might look like the following:
const postState = {
// merged results from PREVIEW api
previews: [1, 2, 3],
// merged results from FULL api
full: [2],
// all merged entities
entities: {
1: {
title: 'foo1'
},
2: {
title: 'foo2',
body: 'bar',
},
3: {
title: 'foo3'
}
}
};
(A) You would have two reducers + actions, one for each representation, to distinguish the entities. Depending on the PREVIEW or FULL posts API request, you would serve one of your reducers via one explicit action. Your sub-states might look like these:
const previewPostState = {
// merged results from PREVIEW api
result: [1, 2, 3],
// all preview entities
entities: {
1: {
title: 'foo1'
},
2: {
title: 'foo2',
},
3: {
title: 'foo3'
}
}
};
const fullPostState = {
// merged results from FULL api
result: [2],
// all full entities
entities: {
2: {
title: 'foo2',
body: 'bar'
}
}
};
From a very high level perspective you can already see that you would have to save duplicated information. The post entity with id: 2
would be saved two times with its title property: one time for previewPostState
and one time for fullPostState
. Once you want to change the title property in your global state, you would have to do it at two places. One would violate the single source of truth in Redux. That's the reason I would go with choice (B): You have one place for your post entities, but can distinguish clearly their representations by your result arrays.
Upvotes: 3
Reputation: 7026
Think of the app's state as a database. I suggest you to use this state shape:
{
entities: {
// List of normalized posts without any nesting. No matter whether they have all fields or not.
posts: {
'1': {
id: '1',
title: 'Post 1',
},
'2': {
id: '2',
title: 'Post 2',
}
},
},
// Ids of posts, which need to displayed.
posts: ['1', '2'],
// Id of full post.
post: '2',
}
First of all, we are creating our normalizr
schemas:
// schemas.js
import { Schema, arrayOf } from 'normalizr';
const POST = new Schema('post');
const POST_ARRAY = arrayOf(POST);
After success response, we are normalizing response data and dispatching the action:
// actions.js/sagas.js
function handlePostsResponse(body) {
dispatch({
type: 'FETCH_POSTS',
payload: normalize(body.result, POST_ARRAY),
});
}
function handleFullPostResponse(body) {
dispatch({
type: 'FETCH_FULL_POST',
payload: normalize(body.result, POST),
});
}
In reducers, we need to create entities
reducer, which will be listening all actions and if it has entities
key in payload, would add this entities to the app state:
// reducers.js
import merge from 'lodash/merge';
function entities(state = {}, action) {
const payload = action.payload;
if (payload && payload.entities) {
return merge({}, state, payload.entities);
}
return state;
}
Also we need to create corresponding reducers to handle FETCH_BOARDS
and FETCH_FULL_BOARD
actions:
// Posts reducer will be storing only posts ids.
function posts(state = [], action) {
switch (action.type) {
case 'FETCH_POSTS':
// Post id is stored in `result` variable of normalizr output.
return [...state, action.payload.result];
default:
return state;
}
}
// Post reducer will be storing current post id.
// Further, you can replace `state` variable by object and store `isFetching` and other variables.
function post(state = null, action) {
switch (action.type) {
case 'FETCH_FULL_POST':
return action.payload.id;
default:
return state;
}
}
Upvotes: 8