Today I Learned

React Redux code-smells: Data preparation in component

If you have Redux in React project you should not handle data preparation in components. Why? Take a look on example below.

export function MyComponent({ userId, uniqueness }) {
  const user = useSelector((state) => getUser(state, userId));
  const selectedFilter = useSelector(getFilter);
  const selectedSorting = useSelector(getSorting);

  // in function components we can use useMemo for optimization
  // but in class components we would not be able to optimize this properly
  const answers = useMemo(() => {
    if (!user) return [];
    const uniqAnswers = uniqBy(user.answers, uniqueness);
    const filteredAnswers = filter(uniqAnswers, selectedFilter);
    return sortBy(filteredAnswers, selectedSorting);
  }, [user, selectedFilter, selectedSorting]);

  if (!user) return "Loading";

  return (
    <div>
      {user.name}
      {answers.map((answer) => (
        <div key={answer.id}>{answer.description}</div>
      ))}
    </div>
  );
}

If we ever need to display answers, we would need to copy-paste the logic, or extract it to the reusable hook which still would not be the perfect solution.

Wherever possible preparation of data should be handled by the selector. Even if the data comes from different reducers, or component state/props.

That way:

  • Code can be reused in other places
  • Component has less code
  • Reselect selectors have better performance by default
const getUserWithSortedAnswers = createSelector(
  getUser,
  getFilter,
  getSorting,
  (_, _2, uniqueness) => uniqueness,
  (user, selectedFilter, selectedSorting, uniqueness) => {
    if (!user) return [];
    const uniqAnswers = uniqBy(user.answers, uniqueness);
    const filteredAnswers = filter(uniqAnswers, selectedFilter);
    const sortedAnswers = sortBy(filteredAnswers, selectedSorting);

    // in this example sorted answers are modified in user object
    // but this can be done as separate selector eg. getSortedAnswers
    return { ...user, answers: sortedAnswers };
  }
);

export function MyComponent({ userId, uniqueness }) {
  // selector can receive component data from state or props (uniqueness)
  const user = useSelector((state) => 
     getUserWithSortedAnswers(state, userId, uniqueness)
  );

  if (!user) return "Loading";

  return (
    <div>
      {user.name}
      {user.answers.map((answer) => (
        <div key={answer.id}>{answer.description}</div>
      ))}
    </div>
  );
}

If you do not have redux in your React project you can do data preparation in reusable hooks or in wrapper components. As it also provides some reusability and decreases component size.