I'm using ngrx store, to maintain application state, normalizr to flatten my data from API calls and Immutable. So far it's been working really well, but I'm getting to some more complex data relationships and I was wondering how to proceed with structuring the store.
To simplify things, I have two sets of objects. Sessions, and Invoices.
One user logs on and can view his list of Sessions. The state in the store is held in an object ISessions
:
export interface ISessions extends Map<String, any> {
result: List<Number>;
entities: {
sessions: Map<Number, ISessions>,
clients: Map<Number, IClient>,
tutors: Map<Number, IProfile>
},
adding: boolean;
loading: boolean;
loadingFailed: boolean;
error: string;
}
(entities contain the normalized output - clients and tutors are the nested types contained in sessions)
This works really well. The reducer sets it to loading so the view can display loading bars, then the data is populated I can use it in a sensible flat mannor referencing id's in the mapped data.
They can load invoices, this works very similarly around an IInvoices
object:
export interface IInvoices extends Map<String, any> {
result: List<Number>;
entities: {
invoices: Map<Number, IInvoice>,
clients: Map<Number, IClient>,
tutors: Map<Number, IProfile>
},
adding: boolean;
loading: boolean;
}
So my store looks like this:
export interface IAppState {
sessions: ISessions;
invoices: IInvoices;
}
However, now I've come to the more complex relationship. The Sessions are assigned to Invoices. There's a few ways of going forward:
Each of the Invoices could themselves have an ISessions object. This seems to go against the idea of having the data structure flat. I'd also likely have repeat sessions, stored in the AppState.sessions and the AppState.invoices. However it would be easier to manage as the IInvoice more directly maps to the state of the view (loading the sessions etc is stored in the invoices ISessions object all encapsulated).
I could store a map of ISessions to invoice IDs in a the store separately to the ISessions and invoices:
eg:
export interface IAppState {
sessions: ISessions;
invoices: IInvoices;
invoicesSessions: Map<number, ISessions>;
}
There's also the question of whether I should be storing two separate lists of clients and tutors, one in ISessions and one in IInvoices. Is splitting the store like this a bad idea? This would mean my reducers would have to all operate on the whole IAppState
object rather than the sub-sections.
Basically: When I get data in, should I be stripping it out and compiling big ID-indexed lists, letting the view's then almost 'query' what they need - basically using the store like a database OR should I be holding a deep-nested collection of objects that directly reflect views - meaning data is often repeated where it's needed multiple times?
Yes, the recommended approach for structuring your Redux store is to build your state shape in terms of your data, not your views, and the recommendation for relational data is to keep it all normalized. Use selector functions to act as queries into that state.
For more info, see:
Also, my "Practical Redux" tutorial series shows how to use the Redux-ORM library to manage relational data in your Redux state: Practical Redux-ORM Basics and Practical Redux, Part 2: Redux-ORM Concepts and Techniques.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With