If you want to include an optional property in your model interface or type, consider adding a substate name. By doing this, you won't need to specify default:
in the @State
decorator and still be able to access the substate from the parent component.
Here is an example of how it can be implemented:
/*
Please keep in mind that this is just an example ;)
A substate with only one property may not always be sufficient...
Also note that I am using types instead of interfaces
(it's not called InterfaceScript, right? ;) ) - should work the same with interfaces
*/
// Define the name of the sub-state to maintain typing consistency in state models, contexts, etc.
export const selectedBookStateName = 'selectedBook';
export type Book = { title: string };
export type LibraryStateModel = {
books: Book[];
loading: boolean;
// This particular line is where the magic happens
// `[variable]: type` means "use the value of the variable as the property name"
// `foo?: type` means "the property foo is optional"
// Together, we create an "optional property named after the value of selectedBookStateName"
[selectedBookStateName]?: SelectedBookStateModel;
};
export type SelectedBookStateModel = { model: Book | null; };
// Helper state operator allows us to easily manage the selected book substate without worrying about its actual value
export function patchSelectedBookSubState(
selectedRepository: Partial<SelectedBookStateModel>
): StateOperator<LibraryStateModel> {
return patch({
[selectedBookStateName]: patch<SelectedBookStateModel>(selectedRepository),
});
}
@State<LibraryStateModel>({
name: 'library',
defaults: {
books: [],
loading: false,
// Not specifying "selectedBook" here is optional; no need to do so
},
children: [SelectedBookState],
})
@Injectable()
export class LibraryState {
@Action(SelectBook)
selectBook(ctx: StateContext<LibraryStateModel>, action: SelectBook) {
const state = ctx.getState();
// The substate is expected to be undefined
// It should never be, but we lose this information when making the substate optional in our library type.
// I chose not to solve this issue (and it doesn't bother me)
const current: SelectedBookState | undefined = state.selectedBook?.model;
// Another way to access the substate
const current2: SelectedBookState | undefined = state[selectedBookStateName]?.model;
// The NGXS documentation advises against using setState as it would cause the context of the substate to be erased.
// However, this is not applicable here since we returned `patch()` from our helper state operator.
ctx.setState(patchSelectedBookSubState(action.payload));
}
@State<SelectedBookStateModel>({
// Using the constant defined earlier
name: selectedBookStateName,
defaults: {
model: null
},
})
@Injectable()
export class SelectedBookState {
@Action(BorrowBookOrSomething)
borrowBook(ctx: StateContext<SelectedBookStateModel>) {
// Implementation goes here...
}
}