In a React application, the render components are responsible for receiving user input and displaying the UI state. When it comes to storing the UI state there are two options: we can do this inside the component as well, or we can store it externally. I prefer to store UI state in so-called containers that are external to the component, for three reasons:
In the remainder of this article I will elaborate on these points. To explain my approach for storing the UI state, I will use the example of Vidlito (video-list-tool): an application that allows people to store collections of playlists that contain video clips. Broadly speaking, my approach is the following:
playlistsCtr
, and clipsCtr
).applyFilter
callback of the filtering facet can automatically correct the
highlight, so that the highlighted clip is not hidden by the filter.Let's look at the code for clipsCtr
and its facets, as well as the mapClipsData
function that maps data between these facets.
import { Addition, Deletion /* other imports omitted for brevity */ } from 'skandha-facets';
const clipsCtr = {
addition: new Addition<ClipT>(),
deletion: new Deletion(),
display: new Display<ClipT>(),
dragAndDrop: new DragAndDrop(),
edit: new Edit(),
filtering: new Filtering<ClipT>(),
highlight: new Highlight<ClipT>(),
insertion: new Insertion<ClipT>(),
// This facet is used to move a clip to another playlist
move: new Move(),
selection: new Selection<ClipT>(),
store: new Store<ClipT>(),
};
import { mapDataToProps } from 'skandha';
export function mapClipsData(clipsCtr) {
const ctr = clipsCtr;
mapDataToProps(ctr, {
display: { items: () => ctr.insertion.preview },
filtering: { inputItems: () => ctr.store.items ?? [ ] },
highlight: {
highlightableIds: () => getIds(ctr.display.items),
item: () => getClipById(ctr.highlight.id),
},
insertion: {
inputItems: () => ctr.filtering.outputItems,
preview: () => { /* omitted for brevity */ },
selection: {
selectableIds: () => getIds(ctr.display.items),
items: () => ctr.selection.ids.map(getClipById).filter((x?: ClipT) => !!x) as ClipT[],
},
});
}
Each facet has its own API that contains data fields and operations. For example, the selection
facet has fields
ids
, selectableIds
, anchorId
and items
, as well as the select
operation (we will take a closer look
in the next section). You can think of the Selection
facet class as an abstraction: a very general description of what
selections look like and how they are used. Like all abstractions, this helps with promoting code consistency and reuse:
regardless of how selection is implemented, it will look the same everywhere in the application.
It also means we can create generic helper functions that take a selection as their argument.
In many cases, the output of one facet is used as the input of another facet. Therefore, we can use the mapDataToProps
function to map data between facets. This function patches a facet by
replacing a data field by a get
property. In the example code above, we want to select from the displayed items. Therefore,
mapDataToProps
is used to replace the ctr.selection.selectableIds
field by a property that returns getIds(ctr.display.items)
.