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>
);
});TodolistsStateProviderA 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.