Separating state from presentation using state providers
August 2023, by Maarten Nieber
State providers

Outlook

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.

State providers

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:

  • It keeps the "view" components simple and focused on rendering;
  • It prevents synchronization problems with different components fetching data at different times (which can also lead to overfetching);
  • It makes it easier to share complex state between components.

Adding state providers to the component tree

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.

routes/components/UrlRouter.tsx
export const UrlRouter = observer((props: PropsT) => {
  return (
    <Switch>
      <TodoListsStateProvider>
        <Route path="/todolists">
          <TodolistsView />

          <Route path="/todolists/:todolistSlug">
            <TodoListsStateProvider>
              <TodosView />
            <TodosStateProvider>
          </Route>
        </Route>
      </TodoListsStateProvider>
    </Switch>
  );
});

The TodolistsStateProvider

A state provider does three things:

  • It instantiates a state object;
  • It caches certain parts of the state and adds resource states to them;
  • It provides the state as a set of default properties (to all components wrapped in the StateProvider).

Here is an example:

todolists/components/TodolistsStateProvider.tsx
todolists/hooks/useTodolistsState.tsx
todolists/TodolistState.ts
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>
  );
});

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:

  • The updateSources function is used to return a resource that has a resource state;
  • An ObservableQuery is used to track the loading state of the getTodolists query;
  • The 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;
  • Different parts of the TodolistsState, such as Selection, Highlight and Deletion are captured in facets. To keep the example relatively simple, I've included Deletion but not Addition.