-
Notifications
You must be signed in to change notification settings - Fork 19
Redux in Caseflow
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.
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:
- Redux values are accessible throughout the React component tree for the entire application within the Redux Store.
- 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.
Within Caseflow, an individual Redux store consists of these parts:
- Constant list of action names
- List of Actions (using the constants for the type of the action
- 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:
- createStore: Builds Redux store for application. Needs an initial state and reducer
- Provider: wrapper that wraps application to pass store values to child components
- combineReducers: reducer that combines multiple reducers into one. Usually passed into createStore as the reducer value
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.
- queueStore.js
- hearingsStore.js
- testUtils.js (intake store)
- WrappingComponent.jsx (only used in tests. example of wrapper)
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.)
- root.js (Reader)
- index.js (Reader)
- reducers.js (Reader)
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
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
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
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:
- Import action into the component file
-
mapDispatchToProps
dispatches your actions to the Redux store and maps the actions to your props. -
mapStateToProps
pulls a value from the store and assigns it to whatever value you want to set it to. In the example above, thestate.intakeCorrespondence
is tapping into the current state of theintakeCorrespondence
redux store. - 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 likethis.props.action-name-here
.
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.
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.
- 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
- Use
- 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.
- Home
- Acronyms and Glossary
- Caseflow products
- Caseflow Intake
- Caseflow Queue
- Appeals Consumer
- Caseflow Reader
- Caseflow eFolder
- Caseflow Hearings
- Caseflow Certification
- Caseflow APIs
- Appeal Status API
- Caseflow Dispatch
-
CSUM Roles
- System Admin
- VHA Team Management
- Active Record Queries Resource
- External Integrations
- Caseflow Demo
- Caseflow ProdTest
- Background
- Stuck Jobs
- VA Notify
- Caseflow-Team
- Frontend Best Practices
- Accessibility
- How-To
- Debugging Tips
- Adding a Feature Flag with FeatureToggle
- Editing AMA issues
- Editing a decision review
- Fixing task trees
- Investigating and diagnosing issues
- Data and Metric Request Workflow
- Exporting and Importing Appeals
- Explain page for Appeals
- Record associations and Foreign Keys
- Upgrading Ruby
- Stuck Appeals
- Testing Action Mailer Messages Locally
- Re-running Seed Files
- Rake Generator for Legacy Appeals
- Manually running Scheduled Jobs
- System Admin UI
- Caseflow Makefile
- Upgrading Postgresql from v11.7 to v14.8 Locally
- VACOLS VM Trigger Fix M1
- Using SlackService to Send a Job Alert
- Technical Talks