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 theuseGames
fetching hook, promoting cleaner and more maintainable code.The
App
component is then refactored to manage a singlegameQuery
state.The
GenreList
andPlatformSelector
components receive and update thegameQuery
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!