Write an Action
You use actions to trigger an update of the Redux state. Actions don't update the state directly, but use reducers, which describe exactly how the state should change in response to a given action.
A reducer is a function that takes the previous state and an action and returns the next state. Communications Open Storefront Framework provides a number of default actions. You can find them in the @oracle-dx4c-buying/actions/<action_type> directory. If you can't find an action that meets your needs, create an action for your business scenario and store it in this directory.
When you write an action, you specify the type of action that you expect the application to perform, such as get state. The application performs that action by calling a Buying and Billing API endpoint. The application uses Redux-Saga, which enables asynchronous calls, to make calls to the Buying and Billing API endpoints. Once the endpoint sends it response, the application takes the endpoint's response and places it in a specific part of the store. This triggers the selectors that listen to that part of the store and renders the updated data into the UI component.
In the Plans widget example, we assumed that the promotionOfferings object is automatically populated in the Redux state. Now, let's see how you can use an action to populate the same object in the Redux state. To do so, you write an action named _getPlans in the actions/plan/index.js file. Here's the sample getPlans action:
import {takeLeading, createReducer, combineReducers} from '@oracle-cx-commerce/store/utils';
import {LIST_PRODUCT_OFFERINGS} from '@oracle-dx4c-buying/common/endpoint/endpoints';
import {callEndpointSaga} from '@oracle-dx4c-buying/common/endpoint/endpoint-saga';
import {parsePlans} from './parser/plan-parser';
function* getPlansSaga(action) {
return yield callEndpointSaga(action, LIST_PRODUCT_OFFERINGS, parsePlans);
}
function loadingReducer(state /*, action*/) {
return {...state, loading: true};
}
export default {
reducer: combineReducers({
dx4cRepository: combineReducers({
planContext: combineReducers({
planSearch: createReducer({
_getPlans: loadingReducer
})
})
})
}),
*saga() {
yield takeLatest('_getPlans', getPlansSaga);
}
};
The getPlans action contains a reducer function called loadingReducer, which populates a loading mark in the state when the data is retrieved from the endpoint. Most of the actions in the communications storefront application follow the same pattern. Here's the sample loadingReducer function:
function loadingReducer(state /*, action*/) {
return {...state, loading: true};
}
reducer: combineReducers({
dx4cRepository: combineReducers({
planContext: combineReducers({
planSearch: createReducer({
_getPlans: loadingReducer
})
})
})
Now this function is called when the _getPlans action is triggered. The state once the action is complete looks similar to the following:
dx4cRepository: {
planContext: {
planSearch: {
loading: true
}
}
}
In our example, the getPlans action uses a saga (Redux-Saga), which updates the state when the data is retrieved from the endpoint. However, you can write corresponding reducers as needed when a particular action is complete.
*saga() {
yield takeLatest('_getPlans', getPlansSaga);
}
Sagas use the takeEvery and takeLatest functions to update the state. The takeEvery function allows multiple instances to be started concurrently. The takeLatest function allows only the latest request to be triggered (displaying the latest version of data). It only allows one task to run at any moment.
When you create an action that uses the saga function, the application adds it to the saga middleware to call an endpoint.
In our example, the getPlansSaga calls the LIST_PRODUCT_OFFERINGS endpoint to complete the action:
import {LIST_PRODUCT_OFFERINGS} from '@oracle-dx4c-buying/common/endpoint/endpoints';
import {callEndpointSaga} from '@oracle-dx4c-buying/common/endpoint/endpoint-saga';
import {
transformProductOfferingsForPlanSearch,
} from '@oracle-dx4c-buying/actions/transform/product-offering/product-offering';
function* getPlansSaga(action) {
return yield callEndpointSaga(action, LIST_PRODUCT_OFFERINGS, transformProductOfferingsForPlanSearch);
}
}
In this file, you can see that the getPlansSaga also passes a transform callback function to the callEndpointSaga. Transform functions typically take the response from the API endpoint and convert them into a compact model that's more suitable for the UI development. The endpoint places the output of this transform operation directly into the state. In this case, the transform function plays the role of the reducer by updating the appropriate part of the state.
Here's the sample transformProductOfferingsForPlanSearch function:
export function transformProductOfferingsForPlanSearch(plans, state) {
return {
dx4cRepository: {
planContext: {
planSearch: {
loading: false,
...transformProductOfferingsIntoPlans(plans, state)
}
}
}
}
}
}
You can find the specifics of the transformProductOfferingsIntoPlans function in the @oracle-dx4c-buying/actions/utils/transform/product-offering/product-offering.js file. The transform function generates similar output for the Plans widget:
dx4cRepository: {
planContext: {
planSearch: {
loading: false,
promotionOfferings: [...] //This is the output of the transform function above
}
}
}
Note that the loading flag has been set to false in this output to indicate that the data loading is complete.
Once you create the action, you can add it in the Plans index.js file to retrieve the plan data. Here's the sample Plans index.js file with the getPlans action:
import React, {useEffect, useContext} from 'react';
import Styled from '@oracle-cx-commerce/react-components/styled';
import {connect} from '@oracle-cx-commerce/react-components/provider';
import {StoreContext} from '@oracle-cx-commerce/react-ui/contexts';
import {getPromotionOffering} from '@oracle-dx4c-buying/common/selector';
import Plan from '../plan';
import css from './styles.css';
function Plans(props) {
const {promotionOffering} = props;
const {action} = useContext(StoreContext);
useEffect(() => {
action('_getPlans', {
type: 'package',
limit: 50,
expand: 'productOfferingPrice',
priceListName: 'DX4C NA Pricelist'
});
}, [action]);
return (
<Styled id="Plans" css={css}>
<div>
<div className="plans-container">
{promotionOffering &&
promotionOffering.map(promotion => {
return (
<div key={`promotion_${promotion.id}`}>
<Plan promotion={promotion} {...props} />
</div>
);
})}
</div>
</div>
</Styled>
);
}
export default connect(getPromotionOffering)(Plans);
You can also add a useEffect to trigger the getPlans action. This hook automatically triggers the getPlans action when the corresponding component is initialized. Once the object in the state is updated, the Plans widget redisplays the plan data on the UI.
Here's a sample useEffect for triggering the getPlans action:
const {action} = useContext(StoreContext);
useEffect(() => {
action('_getPlans', {
type: 'package',
limit: 50,
expand: 'productOfferingPrice',
priceListName: 'DX4C NA Pricelist'
});
}, [action]);