Using Redux in UI Components

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.

  • #frontend
  • #redux
  • #typescript