How to Organize Model Type with TypeScript and React/Redux
Posted on: 2018-05-29
I will not pretend that there is a universal way organized type in a Redux/React application. Instead, I'll present what, in the project I am actually working in my day to day job, I found an easy, clean and clear way to organize types.
First, let's establish that other than all the business logic types you need that React require to have at a minimum a type for your React's property. I'll skip the React's state property mostly because I rarely rely on the state but also because it is not a big deal. You can handle the state's type with a type or interface directly in the React's component file since it will only be used internally for this component.
Second, let's pretend we are working on a normalized model. A normalized model means that Redux will store only a single instance of each entity in the store -- there is no duplication of data. A normalized model infers that the data will be denormalized during the mapping from Redux store to your React's components. The normalized model will have an id (string or number) instead of having the object. For example, if you have EntityA that has a one-to-many relationship to EntityB, than the EntityA in the normalized model will have an array of EntityB ids, not the EntityB instance. However, in the EntityA denormalized you will have an array of EntityB. The normalized doesn't have duplicate, the denormalized has the possibility to do EntityA.ArrayOfB[0].Name because EntityB is rich and complete while the normalized is just a key.
Third, React uses properties to hydrate the component and properties to provide actions. Separating the behaviors and the data model will be a natural choice if you are using React-Redux library as we will see soon.
With the prerequisite that we have a model divided in two (normalized and denormalized) and that we are using React (property) that use your business logic, it starts to be clear that for a specific entity we will have many types of interfaces and some values will cross-over. In fact, all properties that are not a relationship will be used in the normalized and denormalized definition.
The construction for each entity that is normalized and denormalized is to have an entity that contains no relation, one that contains the relationship keys, and one that contains the rich objects which will be filled during the mapping to React. For example, if you have "EntityA", the pattern would be to have "EntityA" and "EntityANormalized" and "EntityADenormalized" that inherit "EntityA". During the mapping (and the creation of EntityADenormaliez) you use all the common property from "EntityA" which reside inside Redux's store that hold instances of EntityANormalized and you remove all keys and array of keys to replace them with the other object in the store. For example, if you have EntityA that has a relationship to B, the EntityANormalized have "EntityB:number" which won't be used in EntityADenormalized because this one will have "EntityB:EntityBDenormalized". Once you have these three interfaces created, you can create a EntityA model which contain a 1-1 relationship to the denormalized entity but also can have other data needed in the React component. For example, you can have Routing data, other denormalized entities, or global user's preference data, etc. The fifth interface contains a list of all potential action the user can execute in the component. Finally, a simple interface that extends the Model and Dispatch interface is created and used by the React component has its property.
The final result is of all the interface created look like this UML diagram:
The advantage of this modeling is the reusability for the base class (EntityA) by the normalized and denormalized. It is also clear to all developers that code in the system that these fields are coming from the backend and are "values" while the normalized contains the relationship keys. The mapping contains the logic to denormalize the object providing to React a rich model that has a good navigability in the properties of all objects but also contains fields that might be dynamically computed during the mapping. Finally, the division of model and dispatch work flawlessly with React-Connect because the connect function require to pass each type. It is also convenient because if you have a hierarchy of component you can pass only action or a set of model depending to which children React components have.
Here is an example of how React-Redux's connect function takes the model and dispatch types as well as how the React component for EntityA uses the property.
export default connect<EntityAModel, EntityADispatch, {}, {}, ReduxState>( (s) => entityAMapper.mapStateToProps(s),
(s) => entityAMapper.mapDispatchToProps(s),
(stateProps, dispatchProps, ownProps) => Object.assign({}, ownProps, stateProps, dispatchProps), {
pure: true,
areStatesEqual: (n, p) => entityAMapper.shouldMappingBeSkipped(n, p)
}
)(EntityA)
class ComponentA extends React.Component<ComponentAProps> { }