A quick look at the tools, libraries, and frameworks I use to build modern React applications, from state management to styling, and everything in between. Here’s a quick breakdown of the tools and libraries I use regularly in production React apps — and when I use each one.
For: Asynchronous data fetching and caching.
React Query simplifies data management by abstracting fetch logic, caching, background updates, and stale handling. It avoids redundant requests and keeps the UI in sync with the backend state with minimal boilerplate.
import { useQuery } from "@tanstack/react-query"
function Users() {
const { data, isLoading } = useQuery(["users"], () =>
fetch("/api/users").then(res => res.json())
)
if (isLoading) return <div>Loading...</div>
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
For: Centralized query options management.
This utility helps structure React Query configurations consistently — especially useful when managing a growing number of endpoints or shared query behaviors like stale time, retry logic, and suspense settings.
import { createBuilder } from "@ibnlanre/builder"
interface AuthPayload {
username: string
password: string
fcm_token: string | undefined
}
const builder = createBuilder({
auth: {
login: (payload: AuthPayload) => axios.post("/auth/login", payload),
},
})
// Integrating with React Query
const { data, status } = useQuery({
queryKey: builder.auth.login.$get,
queryFn: builder.$use.auth.login,
})
For: Type safety and DX.
It catches errors before runtime, improves autocomplete, and makes code self-documenting. Every piece of logic is clearer and safer when types are enforced across components, services, and APIs.
type User = {
id: string
name: string
}
const user: User = { id: "1", name: "John" }
I use these depending on the use case:
Each has its niche. Next.js excels in SEO-driven apps, while TanStack and React Router are lighter and better for highly interactive frontends.
For: Lightweight state management.
Used when state is local to a part of the app — like theme toggles, user preferences, or modal state. It’s simple, built-in, and avoids external dependencies.
For: Complex, global state management.
When state needs to be shared across many components and involves normalized data, side effects, or persistence (e.g., forms, wizards, user sessions), I use Redux Toolkit. It reduces boilerplate and integrates cleanly with TypeScript.
import { configureStore, createSlice } from "@reduxjs/toolkit"
const counterSlice = createSlice({
name: "counter",
initialState: 0,
reducers: {
increment: state => state + 1,
},
})
export const { increment } = counterSlice.actions
const store = configureStore({
reducer: { counter: counterSlice.reducer },
})
For: UI components and layout.
Mantine provides accessible, customizable, and modern components out of the box. Its hooks (e.g. for modals, notifications) save time, and the styling API works well alongside Tailwind when needed.
import type { PropsWithChildren } from "react"
import { MantineProvider, createTheme } from "@mantine/core"
import { Notifications } from "@mantine/notifications"
const theme = createTheme({
colors: {
app: [], // Array with 10 colors to generate the color palette for this color scheme
},
})
const Providers = ({ children }: PropsWithChildren) => {
return (
<MantineProvider theme={theme}>
<Notifications position="top-center" autoClose={5000} />
{children}
</MantineProvider>
)
}
For: Utility-first styling.
I use Tailwind for layout, spacing, and utility classes — especially for quick prototyping and consistent design systems. It pairs well with Mantine by covering layout and grid systems, while Mantine handles component logic.
<button class="bg-blue-500 text-white py-2 px-4 rounded-lg">Submit</button>
This stack isn’t set in stone. It evolves as projects grow and requirements shift. But this combination gives me control, performance, and developer speed — without sacrificing scalability or maintainability.