Redux in Caseflow - department-of-veterans-affairs/caseflow GitHub Wiki

Caseflow Redux Best Practices

As well as using React on the frontend, Caseflow also utilizes Redux to manage state across the different applications within Caseflow (Queue, Intake, Reader, etc.) Building a Redux store can sometimes be complex to set up, but is a powerful tool that can fill in the gaps of React's state management.

What is Redux and How Does it Work?

Redux is a state management library that works with other JS libraries/frameworks like React, Angular, or Vue. The strength of Redux is managing state across an application.

Redux state is read-only. To update the state of the application (aka Redux Store), an action (change to state) needs to be explicitly dispatched. Dispatched actions are then consumed by reducers which apply the dispatched actions to the state of the application

The benefits of Redux are:

  1. Redux values are accessible throughout the React component tree for the entire application within the Redux Store.
  2. Unlike React, values aren't dependent on internal component state or inheritance from components. This gives developers more control in updating state with Redux vs with React.

Parts of Redux (Visual)

image

Key Parts of Redux (Big Picture)

Within Caseflow, an individual Redux store consists of these parts:

  1. Constant list of action names
  2. List of Actions (using the constants for the type of the action
  3. Reducer (switch statement checking Action type and updating the state based on the Action called.

To wrap an application (or React component tree) in a Redux store, the Redux store will need these parts:

  1. createStore: Builds Redux store for application. Needs an initial state and reducer
  2. Provider: wrapper that wraps application to pass store values to child components
  3. combineReducers: reducer that combines multiple reducers into one. Usually passed into createStore as the reducer value

image

Examples of Redux Stores within Caseflow

These are examples of stores being called within their respective application. The store wraps the top of the React component tree with a <Provider> so that values can be pulled from any child component within the application tree. In Caseflow, we use the tag as a wrapper to most applications, as it was specifically created as a wrapper containing the provider + store.

  1. queueStore.js
  2. hearingsStore.js
  3. testUtils.js (intake store)
  4. WrappingComponent.jsx (only used in tests. example of wrapper)

Examples of combineReducers in Caseflow

combineReducers are used to take individual Redux actions/reducers and combine them together to maintain the state within a whole application (Queue, Intake, Reader, etc.)

  1. root.js (Reader)
  2. index.js (Reader)
  3. reducers.js (Reader)

Building Redux Action lists

In Caseflow, Redux Actions are contained to a single Actions file for each Redux store/reducer. Each action is exported and called within the React component where the state update needs to occur. Each Action has a type and a payload. The type comes from a CONSTANTS list of action types. The payload is the value within the store that you are updating. Actions generally follow this syntax:

export const actionName = (payloadValue) =>
  (dispatch) => {
    dispatch({
      type: ACTIONS.CONST_ACTION_NAME,
      payload: {
        payloadValue
      }
    });
  };

// more actions below in file

Building Redux Reducer

Reducers are switch statements that handle every ACTION within the Action list of the store. The Reducer needs an initial state to be defined. Once created, the state is updated when actions within the list are called. Depending on the action, the corresponding value in the store is updated with the payload of the action. The final case of the switch statement is a default return. Within Caseflow, the React update immutability helper is commonly used to make the syntax more readable.

import { update } from '../../../util/ReducerUtil';
import { ACTIONS } from './exampleActionConstants.js';

export const initialState = {
  storeInitialValue: [],
  // other values go here
};

export const exampleReducer = (state = initialState, action = {}) => {
  switch(action.type) {
  case ACTIONS.EXAMPLE_ACTION_NAME:
    return update(state, {
      storeInitialValue: {
        $set: action.payload.storeInitialValueFromAction
      }
    });
  // other switch actions go here!
  default:
    return state;
  }
};

export default exampleReducer

Example of Building a Redux Store

Occasionally, new Redux stores will need to be built within Caseflow. While there are many parts to a Redux store, building a store is very boiler-plate. This is an example of a Redux store that was set up within Caseflow for Correspondence work. Since Correspondence is part of the Queue application, this store was added to Queue's rootReducer in client/app/queue/reducers.js

Calling Actions in React Class Components

Outside of the redux store (constant, action, reducer), this is what you need for a class component to call actions and start dispatching to your store:

  1. Import action into the component file
  2. mapDispatchToProps dispatches your actions to the Redux store and maps the actions to your props.
  3. mapStateToProps pulls a value from the store and assigns it to whatever value you want to set it to. In the example above, the state.intakeCorrespondence is tapping into the current state of the intakeCorrespondence redux store.
  4. Connect the dispatch and state to Redux store by wrapping the export in a connect() (bottom of the page in example). Call the action from the props like this.props.action-name-here.

Calling Actions in React Functional Components (Hooks) PREFERRED WAY

Functional components are a little different. Instead of mapDispatchToProps you will use the hook useDispatch, and instead of mapStateToProps you will use the useSelector hook to grab information from the redux store and save it to a constant value. An example can be found in this PR. The action created is setNewAppealRelatedTasks and the action is called in AddAppealRelatedTaskView.jsx. You can see in this PR that useSelector is being used to pull information for appeals, taskRelatedAppeals, and newTasks consts from the redux store. For dispatching the action, a constant was created for useDispatch and the action is dispatched to the store on line 56 of AddAppealRelatedTaskView.

useSelector can also be paired with the useState hook like below:

const [valueFromStore, setValueFromStore] = useState(useSelector((state) => state.reduxStore.valueFromStore))

The power of this syntax allows you to pull values from the redux store and set it to a constant. Due to the immutability of Redux, when setValueFromStore is called, the local value is updated but the Redux store value will remain the same until an explicit useDispatch is called. This restricted flow of state gives developers more control in how they pull and update values within the store.

React/Redux Dev Tools

All Caseflow Developers should have these development tools: React Dev Tool for Chrome Redux Dev Tool for Chrome React dev tools allows you to see your component tree and how components are rendering each other, as well as how props are being passed. Redux dev tools gives you a direct view into your store. When an action successfully dispatches, it will create a log for you to see the action, the payload, and the overall state of the Redux store.

Examples

Redux Action

Redux Reducer

Redux Store/Base

Redux Store Interacting with Database

Learned Lessons and Possible Pain Points

  • Do not rely on solutions sourced from the broad internet. While helpful to understanding Redux, the solution in Caseflow works differently from the most readily available examples and solutions that can be found online. If this guide isn't sufficient to solve an issue, reach out to the development team first.
  • Actions are the main driver in behavior. When making a Redux solution, make sure that the actions are clearly defined and understood at the beginning of development.
  • Information within the Redux store is NOT automatically updated on the underlying database. In order to update the database with information that has been changed, added, or deleted within the store's state, an Action must be called. This is also true when database data is changed: it will not be automatically updated on the webpage or in the Redux store's state.
    • Use ApiUtil to call a controller method for database interactions
  • Redux data is immutable. The state will not change unless an Action is called.
  • When updating the state of a store, the state is being replaced by an altered copy of the state. As such the following code snippet shows the proper syntax for updating a store's state. Make note of the update function and the $set variable. return update(state, { isUserAcdAdmin: { $set: action.payload.isUserAcdAdmin } });
  • When creating the initial state for a Reducer, all fields must be declared.
  • When creating the initial state for a Reducer, any fields or data that will be called from or pushed into the database should be set and passed in by a Ruby controller.
  • When creating the initial state for a Reducer, any fields or data that will NOT be called from or pushed to the database should be set as defaults in the definition of the initial state. These will usually be flags that control the behavior or functionality of the Redux store.

Resources

⚠️ **GitHub.com Fallback** ⚠️