Using Redux in UI Components
As a UI developer, my primary focus is the presentation of a website. Knowledge of how to use Redux stores can help build high-quality components that effectively integrate with the stores, making the development process more efficient. By using Redux, container components can be skipped, and manual matching of data from stores to the props of UI components is unnecessary.
Using stores to manage the data in the presentation layer of a website streamlines collaboration with other JavaScript developers, making the development process more efficient. By centralizing data management in a store, all developers working on a project can easily access and modify the data they need, without relying on other developers. This makes it easier to work in parallel, allowing developers to focus on their specific tasks, knowing the necessary data is available in the store. Ultimately, this results in a more efficient and collaborative development process.
Today I learned (TIL) about using React Redux and multiple stores to manage an application's state.
A couple of words about Redux
Redux is a state management library for JavaScript that provides a centralized, predictable state container, making it easier to manage application state. The Redux store is the single source of truth for an application's state, responsible for maintaining the current state and handling state changes.
To change the state of an application, an action is dispatched to the Redux store. Actions are plain JavaScript objects that describe the type of change to make to the state, typically containing a type field and additional data. When an action is dispatched, it is sent to the root reducer function, which calls the appropriate sub-reducer function based on the action type. The sub-reducer function returns a new slice of state that is combined with the existing state to create a new state tree, which is then stored in the Redux store.
A typical Redux application has multiple sub-reducer functions that manage
specific parts of the application's state. The combineReducers
function
combines these sub-reducer functions into a single root reducer function that is
passed to the Redux store. The root reducer function is responsible for calling
the appropriate sub-reducer function based on the action type and combining the
resulting state slices into a single state tree.
In the code example, two stores manage the state of the application:
user-store
and system-store
. The user-store
contains the user's name and
status, while the system-store
contains the system's online/offline status.
These stores are combined using combineReducers
and configured in the
root.ts
file using configureStore
. The useSelector
hook extracts state
from the stores, and the useDispatch
hook dispatches actions to update the
stores. The code example shows how to use the Redux store, actions, and reducers
to create a predictable and maintainable state management system for a React
application.
Let's jump into the code examples
Here's the code for the user-store
:
// stores/user-store.ts
import { createSlice } from '@reduxjs/toolkit';
interface UserState {
name?: string;
isAway?: boolean;
}
const initialState: UserState = {
name: undefined,
isAway: undefined
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setName: (state, action) => ({ ...state, name: action.payload }),
setAway: (state, action) => ({ ...state, isAway: action.payload })
}
});
export const { setName, setAway } = userSlice.actions;
export default userSlice.reducer;
And here's the code for the system-store
:
// stores/system-store.ts
import { createSlice } from '@reduxjs/toolkit';
interface SystemState {
isOnline?: boolean;
}
const initialState: SystemState = {
isOnline: true
};
const systemSlice = createSlice({
name: 'system',
initialState,
reducers: {}
});
export default systemSlice.reducer;
These stores are then combined in root.ts
:
// stores/root.ts
import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import systemStore from './system-store';
import userStore from './user-store';
const rootReducer = combineReducers({
user: userStore,
system: systemStore
});
const store = configureStore({
reducer: rootReducer
});
export type RootState = ReturnType<typeof rootReducer>;
export default store;
In the App.tsx
file, the useSelector
hook is used to extract the state from
the stores, and the useDispatch
hook is used to update the state by
dispatching actions. The isLoading
state is used to indicate whether the data
is being fetched or not, and a sleep
function is used to simulate data
fetching. Once the data is fetched, the setName
and setAway
actions are
dispatched to update the user-store
.
Here's the code for the App.tsx
file:
// App.tsx
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from './stores/root';
import { setAway, setName } from './stores/user-store';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const App: React.FunctionComponent = () => {
const dispatch = useDispatch();
const userInfo = useSelector<RootState, RootState['user']>(
state => state.user
);
const systemInfo = useSelector<RootState, RootState['system']>(
state => state.system
);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
// fetching data simulation
setLoading(true);
sleep(1000).then(() => {
dispatch(setName('John Doe'));
dispatch(setAway(false));
setLoading(false);
});
}, []);
return (
<main>
<h1>React Redux Example</h1>
{isLoading ? (
<div>Loading...</div>
) : (
<div>
<section>
<div>User Name: {userInfo.name}</div>
<div>User Status: {userInfo.isAway ? 'away' : 'active'}</div>
</section>
<section>
<div>
System Health Check: {systemInfo.isOnline ? 'online' : 'offline'}
</div>
</section>
</div>
)}
</main>
);
};
export default App;
Single-Purpose Stores
While you can use as many stores as necessary in your application, it's important to ensure that each store has a specific purpose and handles a dedicated task. Creating large, cumbersome stores that handle everything can make the code more difficult to maintain and understand. Instead, aim to create smaller, more focused stores that are easier to manage and update.
Using smaller, purpose-dedicated stores makes it easier to test your UI components and use them in style guide examples. When dealing with large stores, you may encounter typing issues such as required fields or objects, even if they aren't used in a particular UI component. By creating smaller, more focused stores, you can avoid these issues and ensure that your tests and style guide examples are more accurate, simple, and efficient.
If you want to learn more about how to use stores and mock them in tests and style guides, be sure to check out a future TIL post on this topic.
Conclusion
In conclusion, using Redux stores in a React application can significantly improve the development process, making it more efficient and collaborative. By centralizing data management, developers can focus on building high-quality UI components without worrying about the data management aspect. The combination of stores, actions, and reducers provides a predictable and maintainable way to manage an application's state, allowing for more efficient development and easier collaboration with other developers.