Simplifying State Management in React with the Query Object Pattern

As projects grow, so does the complexity of managing the state and passing arguments between components. In this blog post, we'll explore a practical refactoring approach using a concept called "Query Object Pattern" to enhance the scalability of our codebase.

The Challenge

Consider a scenario of managing game data in a React application, where users can filter games by genre and platform. Initially, we handle this by passing multiple filter parameters across components, leading to increased states and arguments for hooks.

Let's take a look at the starting point:

// Initial App component
function App() {
  const [selectedGenre, setSelectedGenre] = useState<Genre | null>(null);
  const [selectedPlatform, setSelectedPlatform] = useState<Platform | null>(null);

  return (
    // ... rest of the JSX structure
    <GenreList
          onSelectGenre={(genre) => setSelectedGenre(genre)}
    />
    <PlatformSelector
          onSelectPlatform={(platform) => setSelectedPlatform(platform)}
    />
    <GameGrid
          selectedGenre={selectedGenre}
          selectedPlatform={selectedPlatform}
     />

  );
}

// GameGrid component with two arguments
interface Props {
  selectedGenre: Genre | null;
  selectedPlatform: Platform | null;
}

const GameGrid = ({ selectedGenre, selectedPlatform }: Props) => {
  const { data, error, isLoading } = useGames(selectedGenre, selectedPlatform);
  // ... rest of the code
}

// useGames hook with multiple parameters
const useGames = (
  selectedGenre: Genre | null,
  selectedPlatform: Platform | null
) =>
  useData<Game>(
    '/games',
    {
      params: {
        genres: selectedGenre?.id,
        platforms: selectedPlatform?.id,
      },
    },
    [selectedGenre?.id, selectedPlatform?.id]
  );

As our application evolves, we'll inevitably encounter the need for additional variables to manage features like sorting, ordering, and search phrases. However, the approach of adding numerous variables and passing them around quickly leads to code clutter and complexity.

Refactoring using query object pattern

To tackle this issue, we introduce the concept of a query object pattern. This involves encapsulating related variables within an object. We'll create a query object that contains all the necessary information we need to query the games. With this, our code will be cleaner and easier to understand.

To address the challenges:

  • We start by defining a GameQuery interface, encapsulating two parameters, genre and platform. This abstraction reduces the number of arguments passed to the useGames fetching hook, promoting cleaner and more maintainable code.

  • The App component is then refactored to manage a single gameQuery state.

  • The GenreList and PlatformSelector components receive and update the gameQuery state, streamlining their interfaces.

// App component
export interface GameQuery {
  genre: Genre | null;
  platform: Platform | null;
  sortOrder: string;
  searchText: string;
}

function App() {
  const [gameQuery, setGameQuery] = useState<GameQuery>({} as GameQuery);

  return (
    // ... rest of the JSX structure
    <>
      <GenreList
        onSelectGenre={(genre) => setGameQuery({ ...gameQuery, genre })}
      />
      <PlatformSelector
        onSelectPlatform={(platform) => setGameQuery({ ...gameQuery,platform})}
      />
      <GameGrid gameQuery={gameQuery} />
    </>
  );
}

export default App;

GameGrid component receives the gameQuery object as a prop, further reducing the number of arguments in its interface:

//GameGrid component
const GameGrid = ({ gameQuery }: Props) => {
  const { data, error, isLoading } = useGames(gameQuery);
  // ... rest of the code
};

Finally, the useGames hook is adjusted to accommodate the new GameQuery structure:

const useGames = (gameQuery: GameQuery) =>
  useData<Game>(
    '/games',
    {
      params: {
        genres: gameQuery.genre?.id,
        platforms: gameQuery.platform?.id,
      },
    },
    [gameQuery]
  );

Conclusion

In conclusion, by using the query object pattern, we have significantly improved the organization and scalability of our code. As we keep working on our React projects, using patterns like this will be crucial for keeping our codebases organized and strong as they grow.

Thank you for reading! Happy coding!