State Management in React - rs-hash/Senior GitHub Wiki
State Management in React
State is the engine of the React app, since state only updates the UI
-
data-fetching library
for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state
- Caching... (possibly the hardest thing to do in programming)
- Deduping multiple requests for the same data into a single request
- Updating "out of date" data in the background
- Knowing when data is "out of date"
- Reflecting updates to data as quickly as possible
- Performance optimizations like pagination and lazy loading data
- Managing memory and garbage collection of server state
- Memoizing query results with structural sharing
- When a component using useQuery mounts, React Query checks if the query key exists in the cache and is still valid (not expired).
- If the data is cached and valid, React Query returns the cached data to the component without making a network request.
- If the data is not cached or expired, React Query executes the query function to fetch data from the server. Once the data is fetched, it updates the cache with the new data.
- Mutations triggered by useMutation update the server-side data and automatically update related query results in the cache, ensuring that components always display up-to-date data.
- useQuery is a React Query hook used for data fetching. It takes a query key and an asynchronous function (query function) as arguments.
- When a component mounts, useQuery executes the query function to fetch data.
- If the data is already cached and within its expiry period, useQuery returns the cached data without making a network request. Otherwise, it fetches the data from the server and updates the cache.
- Mutations in React Query are used for modifying data on the server, such as creating, updating, or deleting resources.
- React Query provides the useMutation hook to handle mutation operations. It takes a mutation function as an argument and returns a tuple with the mutation function and mutation status (loading, error, data).
- After a mutation is successfully executed (optimistically or after confirmation from the server), React Query automatically updates the affected query results in the cache, ensuring data consistency.
- React Query supports various invalidation strategies to manage cached data and keep it synchronized with the server.
- Manual Invalidation: Developers can manually invalidate specific queries using the queryClient.invalidateQueries method. This triggers a refetch of the invalidated queries.
- Automatic Invalidation: React Query can automatically invalidate cached data based on predefined rules, such as cache expiry times or mutation results. For example, after a successful mutation that modifies data, related queries can be automatically invalidated to fetch fresh data.
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isPending, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query').then((res) =>
res.json(),
),
})
if (isPending) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
the data flow in Redux Toolkit involves
- The
state
, the source of truth that drives our app; - The
view
, a declarative description of the UI based on the current state - The
actions
, the events that occur in the app based on user input, and trigger updates in the state
-
The current Redux application state lives in an object called the store .
const store = configureStore({ reducer: counterReducer })
-
dispatching actions using action creators
store.dispatch({ type: 'counter/increment' })
-
updating the store's state through reducers
-
subscribing to changes in the store's state in components using the useSelector hook.
const someState = useSelector((state) => state.sliceName.someKey);
-
This streamlined process simplifies state management and ensures that components always reflect the latest state from the Redux store
-
State describes the condition of the app at a specific point in time
-
The UI is rendered based on that state
-
When something happens (such as a user clicking a button), the state is updated based on what occurred
-
The UI re-renders based on the new state
-
For Redux specifically, we can break these steps into more detail:
- A Redux store is created using a root reducer function
- The store calls the root reducer once, and saves the return value as its initial state
- When the UI is first rendered, UI components access the current state of the Redux store, and use that data to decide what to render.
- They also subscribe to any future store updates so they can know if the state has changed.
- Something happens in the app, such as a user clicking a button
- The app code dispatches an action to the Redux store, like dispatch({type: 'counter/increment'})
- The store runs the reducer function again with the previous state and the current action, and saves the return value as the new state
- The store notifies all parts of the UI that are subscribed that the store has been updated
- Each UI component that needs data from the store checks to see if the parts of the state they need have changed.
- Each component that sees its data has changed forces a re-render with the new data, so it can update what's shown on the screen