AndrewMcLagan
AndrewMcLagan

Reputation: 13987

Redux normalizr + dealing with reduced responses

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

Answers (2)

Robin Wieruch
Robin Wieruch

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

1ven
1ven

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

Related Questions