Using a Directed Acyclic Graph in a React-Redux Application
By: Natasha Chitnis
Tags: React Redux, React, Graph Theory, Redux

In the diagram above, we can see that ‘yearly.sev1.incidents’ is only calculated from one parent, ‘monthly.sev1.incidents’. However, it is common for a node to influence many others in our DAG. A child node can act as a parent node as the number of levels grow:

A small portion of the DAG from one of our applications

Defining the DAG in Our Application

Our React application contains a static definition for the DAG. We use a JavaScript object to store details about every node used in our application. We can assign this to a variable called nodeDefinitions.

In nodeDefinitions:

Each key is the name of a node.

The value for each key is an object containing the node’s properties, such as its parents.

A definition for every node of our application’s DAG is stored in an object called nodeDefinitions.

‘yearly.sev1.incidents’ (string): The name we gave to a particular node. The node defined in the code snippet above corresponds to the purple box in the previous discussion about Sev1/Sev2/Sev3 incidents.

(string): The name we gave to a particular node. The node defined in the code snippet above corresponds to the purple box in the previous discussion about Sev1/Sev2/Sev3 incidents. ‘is_input’ (boolean): Boolean value indicating if the node’s value comes from user input or from a calculation involving values of other nodes.

(boolean): Boolean value indicating if the node’s value comes from user input or from a calculation involving values of other nodes. ‘parents’ (array): Array of strings detailing the names of immediate parent nodes.

(array): Array of strings detailing the names of immediate parent nodes. ‘calc’ (function): A function definition for how to calculate the node’s value. This property is only present for nodes which are calculated from at least one input field.

(function): A function definition for how to calculate the node’s value. This property is only present for nodes which are calculated from at least one input field. ‘type’ (string): This property tells us more about how the value is calculated and should be displayed on our UI. The node in this example is “numeric”, which gives a hint to the presentation layer of how to display this data. Other possible types could be “percent” or “currency”.

More about the ‘calc’ property:

If the user changes the value of an input field, the ‘calc’ function is called for every child node of the input field that was modified.

The function takes part of the Redux state called valuesOfAllNodes as an argument and returns the new value for the node based on new value(s) of its parent(s) (more on this later). In order to calculate the next value of a child node, we need to know:

1. The new value(s) of its parent(s)

2. Its ‘calc’ property, which is a JavaScript function that returns the new value of the node

You might see the node definition above and ask: “If you include the names of parent nodes, what about a property for the node’s children?”

Good question!

As a developer-friendly optimization, we do not hard-code the ‘children’ array for each key in nodeDefinitions. It would be an unnecessary manual step that naturally increases chances of errors due to typos or accidental omission. Instead, we statically define nodeDefinitions such that every node has a property for its parents. At application startup time, we have a function loop through each key in nodeDefinitions and add a ‘children’ property if the node currently being parsed influences any other node(s).

Consider that we know ‘monthly.sev1.incidents’ is a parent of ‘yearly.sev1.incidents’ given the definition of ‘yearly.sev1.incidents’ above. As a result, ‘yearly.sev1.incidents’ must be a child of ‘monthly.sev1.incidents’.

After defining nodeDefinitions, we call a function which iterates through each key of the object (every node of the DAG) and does the following steps:

1. Check if the key has parents.

'yearly.sev1.incidents': {

'is_input': false,

'parents': ['monthly.sev1.incidents'], //this node has one parent, since it has one key name in its 'parents' array

'calc': function(valuesOfAllNodes) {

return valuesOfAllNodes['monthly.sev1.incidents'] * 12;

},

'type': 'numeric'

}

Suppose we are on the iteration for ‘yearly.sev1.incidents’. We can see from its definition that it has one parent, ‘monthly.sev1.incidents’.

2. For each parent found in (1), initialize its ‘children’ property to an empty array.

nodeDefinitions['monthly.sev1.incidents'].children = [];

As a result, the node definition for ‘monthly.sev1.incidents’ would look like:

'monthly.sev1.incidents': {

'is_input': true,

'parents': [], //no parents, since it is an input field not influenced by anything other than user input

'children': [], //empty 'children' array initialized

'type': 'numeric'

}

3. Add the name of the child node into the ‘children’ array for each parent found in (2):

nodeDefinitions['monthly.sev1.incidents'].children.push('yearly.sev1.incidents');

//Parent: 'monthly.sev1.incidents'

//Child: 'yearly.sev1.incidents'

As a result, the node definition for ‘monthly.sev1.incidents’ would be updated to:

'monthly.sev1.incidents': {

'is_input': true,

'parents': [], //no parents, since it is an input field not influenced by anything other than user input

'children': ['yearly.sev1.incidents'], //child node added since 'yearly.sev1.incidents' is a child of 'monthly.sev1.incidents'

'type': 'numeric'

}

If a node does not influence the calculations of any other node(s), it should have an empty array for its ‘children’ property in nodeDefinitions. In a visual sense, this implies that there are no directed edges from the node leading to any other ones in our DAG.

Definitions for the ‘monthly.sev1.incidents’ and ‘yearly.sev1.incidents’ nodes after populating the ‘children’ property for ‘monthly.sev1.incidents’

Maintaining the Values of Our DAG’s Nodes in the Redux State

Redux is a library which can be used to manage the state of any JavaScript application. By adding Redux to our React application, we are able to maintain the values of our DAG’s nodes in an organized and predictable manner. We utilize the Redux state to have a single source of truth for storing and updating the value of every node. Almost every React component in our application needs to display some variety of values from our DAG and several of our components need to be able to update the values in our DAG as well. By using Redux, we can easily map these values into our React components to display them on our UI. We can also dispatch actions from our React components to the Redux store if we need to change the value of a node in the Redux state.

We store the value of every node in a JavaScript object within our Redux state. We can name this portion of our state as valuesOfAllNodes.

Each key in valuesOfAllNodes is the name of a node, such as ‘yearly.sev1.incidents’. The value of each key is either the user input (if the node corresponds to an input field) or calculated value (if the node is a child node).

let reduxState = {

//other properties of the Redux state

valuesOfAllNodes: {

//other key-value pairs in the format of node_name: value

'monthly.sev1.incidents': 3, //user input

'yearly.sev1.incidents': 36, //calculated value

'monthly.sev2.incidents': 6, //user input

'yearly.sev2.incidents': 72, //calculated value

//more key-value pairs in the format of node_name: value

},

//more properties in the Redux state

};

In general, a change to the value of a parent node should cascade through relevant parts of the directed acyclic graph, recalculating the values of child nodes.

Any modification to the value of an input field will cause the React component to dispatch an action to the Redux store. Here is a brief summary of the steps which would take place after a React component dispatches the action:

1. The action:

Creates a local copy of valuesOfAllNodes.

Updates the key of the input node in this copy with the new value the user inputted.

Breadth-first search (BFS) is subsequently used to traverse our DAG and call the ‘calc’ function on all child nodes of the input field. Each child’s ‘calc’ function is called with the local copy of valuesOfAllNodes. As a result, the calculation for each child node uses the updated value of the parent input node and will be up-to-date.

2. The action passes its local copy of valuesOfAllNodes in its payload to the Redux store.

3. The store calls the appropriate reducing function for the action just dispatched.

4. The reducer returns the payload of valuesOfAllNodes passed from the action.

5. The store saves valuesOfAllNodes returned from the reducer in step 4 in the Redux state.

6. React components which are connected to the Redux store via mapStateToProps would re-render as a response to the change in Redux state. They would receive the newly updated Redux state and pass it down to other components as necessary.