8

Let's say you have a book application using Redux with a few features:

  • Buy books
  • Have a wishlist of books
  • Maintain a list of books you own
  • Send recommendations to friends
  • Review books

Now let's say that all of these different modules, and a common feature across all these modules is the ability to search for books (i.e. searching for books to buy, books to add to a wishlist, books to review, etc).

These search results will be stored across multiple slices of the state tree.

An example of the state tree might look like this:

{
  bookStore: {
    booksSearchResults: [],
    ...,
  },
  wishlist: {
    booksSearchResults: [],
    ...,
  },
  reviews: {
    newReview: {
      booksSearchResults: [],
      ...,
    },
    ...
  },
  ...
}

Are there any best practices around managing such things? Would it be simply to have a booksSearch slice of state and manage that through a shared component?

How about cases where you might need to search for books in multiple places on the same screen (i.e. you might have an autocomplete search feature in the nav bar in addition to a search component in the main part of the application)?

Is another approach to reuse the search logic and somehow have it update different parts of the state (and if so, does something exist to achieve this)?

NRaf
  • 7,407
  • 13
  • 52
  • 91

4 Answers4

5

I have two pieces of advice for you:

  1. Normalize your store.
  2. Don't store your search results in redux.

Normalization

This is well documented, so I won't go too deep into it here. Suffice it to say that normalizing your store will be flexible and help you to reason about your app. Think of your store as you would a server-side database and it will be more reusable than if you were to tailor each section of the state to your views.

One way you could do this for your app:

{
  books: { 
    bookId: { 
      bookDetails,
      reviews: [ reviewId ],
      ownedBy: [ userId ],
      wishlistedBy: [ userId ],
      recommendations: [ recommendationId ]
    }
  },
  users: {
    userId: { 
      userDetails,
      reviews: [ reviewId ],
      wishlist: [ bookId ],
      ownedBooks: [ bookId ],
      friends: [ userId ],
      sentRecommendations: [ recommendationId ],
      receivedRecommendations: [ recommendationId ]
    }
  },
  reviews: {
    reviewId: {
      bookId,
      userId,
      reviewDetails
    }
  },
  recommendations: {
    recommendationId: {
      sender: userId,
      recipient: userId,
      bookId,
      recommendationDetails
    }
  }
}

You may not need all of those relationships, so don't feel like you have to implement all of that. But starting with a base similar to this will make it much easier to add features later.

Resources:

Where to put search results

Search results are more of a view detail than a data concern. You may need to store a historical record of searches, and in that case, you could add a reducer for searches:

{
  searches: [
    {
      searchTerm,
      searchDetails
    }
  ]
}

But even in that case, I don't think I would store the search results. Storing results will limit functionality in a couple cases.

  1. When the user searches, then a new book is added, you wouldn't find the new book on a subsequent search unless you re-run the search (which negates the usefulness of storing results).
  2. Searching needs to be quick and storing results only speeds up repeated searches (which will likely be less common).

So, I consider results a view detail--or, as James K. Nelson calls it, "Control State" (read: The 5 Types of React Application State). Your question doesn't say which view library (if any) you're using, but the concepts here should be applicable regardless of whether you're using React or not.

Search results should be calculated by the view. The view will receive user input, potentially fetch data (which will be added to the store), run some filter function on the Redux state, and display the results.

Luke Willis
  • 8,429
  • 4
  • 46
  • 79
3

There is a good pattern for reusable components in redux applications that would be useful to you. This is assuming the search is using the same api and logic from the redux point of view and one or more reusable components for the logic.

In this example I'll use <AutoCompleteSearch /> and <FullSearch /> as the UI components, each can appear multiple times in the application or even the same page.

The pattern to use here is to assign a unique prop, here named searchId: string to each component, e.g. <AutoCompleteSearch searchId="wishlist" />.

The component then passes this id in the payload of all the search related actions, e.g.

{
  type: SEARCH,
  payload: {
    term: "Harry Potter",
    searchId: "wishlist",
  }
}

The search reducer is structured as a map with the searchId being the key. Example of redux state:

{
  wishlist: {
    inProgress: true,
    searchTerm: "Harry Potter",
    results: [],
  },
  reviews: {
    inProgress: false,
    searchTerm: "",
    results: [],
  },
}

The reducer then selects the part of the state to handle:

const reducer = (state, action) => {
  switch(action.type) {
    case SEARCH:
      return {
        ...state,
        [action.payload.searchId]: {
          inProgress: true,
          searchTerm: action.payload.searchTerm,
          results: [],
        }
      };
    default:
      return state;
  }

For a ready made library that uses this pattern for data loading, see redux-autoloader.

OlliM
  • 7,023
  • 1
  • 36
  • 47
0

If you are able to reuse the search results, meaning the search query is same or shared across components, then you are better off using a shared component. If not, it is advisable to keep the search and search results separate and relevant to the context.

See the examples Don shared with his todos sample. He creates three different lists for All Todos, Completed Todos and Active Todos.

Charles Prakash Dasari
  • 4,964
  • 1
  • 27
  • 46
0

I think that the answer is depends on your need:

  1. Shared search - e.g. items that was searched in any context.
  2. Uniqe search - e.g. items that was searched in specific context.

If you want to autocomolete by first case, I would suggest to combine all the searches arrays, and store the data in objects that give more deatils, such as searchContext and so on. If you want to autocomplete by second case, search results for other contexts does not relevant, so it recommended to use seperate arrays for any search context.

Yossi
  • 445
  • 2
  • 9