In this chapter, we'll continue the example that was used in chapter 2. We'll assume that
the user has a list of todolists that are loaded with useGetTodolists
. The todo's of the
highlighted todolist will be loaded with useTodos
(we will focos on loading the todolists,
and skip over the loading of the todos). We'll see how so-called state providers can be used to
load the data and provide it to the other React components in the component tree.
In general, React components that are on the same page in the application tend to need the same data.
Therefore, I've adopted the approach of using a special StateProvider
component to load all data for
all components that are on the same page. This data is provided to the children via React's context
API.
Note that although a StateProvider
is a React component, it is not responsible for rendering.
I prefer to use a StateProvider
over letting components fetch their own data, for different reasons:
Before we discuss the StateProvider
component in detail, let's look at how it is used
in the rendering tree. The example code below shows a TodolistsStateProvider
component that
fetches the todolists and provides them to TodolistsView
and TodosStateProvider
(and to all other wrapped components). The TodosStateProvider
loads the todo's of the currently
highlighted list, and provides them to the TodosView
component.
export const UrlRouter = observer((props: PropsT) => {
return (
<Switch>
<TodoListsStateProvider>
<Route path="/todolists">
<TodolistsView />
<Route path="/todolists/:todolistSlug">
<TodoListsStateProvider>
<TodosView />
<TodosStateProvider>
</Route>
</Route>
</TodoListsStateProvider>
</Switch>
);
});
TodolistsStateProvider
A state provider does three things:
StateProvider
).Here is an example:
export type PropsT = React.PropsWithChildren<{}>;
export const TodolistsStateProvider = observer((props: PropsT) => {
const { todolistsState, getTodolists } = useTodolistsState({});
const cache = useBuilder(() =>
makeAutoObservable({
get todolists() {
return updateSources(
{ resource: todolistsState.todolistsCtr.data.items },
['loading', () => isQueryLoading(getTodolists), 'getTodolists'],
);
},
get todolist() {
return updateSources(
{ resource: todolistsState.todolistsCtr.highlight.item },
['loading', () => isQueryLoading(getTodolists), 'getTodolists'],
);
},
})
);
const getTodosContext = () => {
return createGetProps({
todolistsState: () => todolistsState,
todolists: () => cache.todolists,
todolist: () => cache.todolist,
todolistsDeletion: () => todolistsState.todolistsCtr.deletion,
todolistsHighlight: () => todolistsState.todolistsCtr.highlight,
todolistsSelection: () => todolistsState.todolistsCtr.selection,
},
});
};
return (
<TodosContext.Provider value={getTodosContext()}>
{props.children}
</TodosContext.Provider>
);
});
export type PropsT = {};
export const useTodolistsState = (props: PropsT) => {
const graftResourceStatesFromMemo = useGraftResourceStatesFromMemo({});
// Mutations. Note that useDeleteTodolists returns a
// ObservableMutation instance.
const deleteTodolists = useDeleteTodolists();
// Queries
const getTodolists = useObservableQuery(useGetTodolists());
const todolistsState = useBuilder(() => {
return new TodolistsState({
//
getTodolists: () => {
return graftResourceStatesFromMemo({
resources: getTodolists.data?.todolists ?? [],
});
},
deleteTodolists: (ids: string[]) => {
const todolists = lookUp(
ids, todolistsState.todolistsCtr.data.itemById
);
return trackPromise({
name: 'deleteTodolists',
states: { updating: todolists },
promise: deleteTodolists.mutateAsync(ids),
}).result;
},
});
}) as TodolistsState;
React.useEffect(() => () => todolistsState.destroy(), [todolistsState]);
return { todolistsState, getTodolists, deleteTodolists };
};
export type PropsT = {
getTodolists: () => TodolistT[];
deleteTodolists: (ids: string[]) => Promise<any>;
};
export class TodolistsState {
props: PropsT;
todolistsCtr = {
deletion: new DeletionWithFlag(),
highlight: new Highlight<TodolistT>(),
selection: new Selection<TodolistT>(),
};
getSummary() {
return {
todolistsCtr: Skandha.getCtrState(this.todolistsCtr),
};
}
destroy() {
Skandha.cleanUpCtr(this);
}
constructor(props: PropsT) {
this.props = props;
registerTodolistsCtr(this);
}
}
The createGetProps
helper function takes an object with functions as values, and returns an object
with get properties. In this example, several parts that are discussed on my blog come together:
updateSources
function is used to return a resource that has a resource state;ObservableQuery
is used to track the loading state of the getTodolists
query;trackPromise
function is used to track the state of the deleteTodolists
mutation, and to reflect
this in the resource state of the todolists that are being deleted;TodolistsState
, such as Selection
, Highlight
and Deletion
are captured in facets.
To keep the example relatively simple, I've included Deletion
but not Addition
.