Loadables - a simple way to load data in React
Tags: javascript, react, webdev, tutorial
Published: 2022-08-15 09:18:15

The real-world app involves data loading via some API and showing UI based on the states of the API. For example, while data is loading, you may show a loader animation, but on error, you may show an error UI. This fairly simple-looking task ends up getting complex super fast and is more difficult to maintain with all the spaghetti code for UI synchronization. So here I propose the **loadable** pattern to simplify data loading and synchronize the UI with it.

In this example, we are going to load a list of todos. Here we are using react-redux as a state management solution. Below we will see how to create a store and reducer with react-redux. However, you can directly skip to ["loadables"](#heading-loadableloadable) if you familiar with react-redux-context store.

### [Create react-redux context store](#react-context-store)

Let's start by creating a react-redux-context-store for storing our todos. The following sample is taken from [react-redux](https://react-redux.js.org/api/hooks#custom-context).

```jsx 
// [filename: todo.store.jsx]

import React from 'react'
import {
  Provider,
  createStoreHook,
  createDispatchHook,
  createSelectorHook,
 from "react-redux";
import { createStore } from "redux";
// reducer for the state
import { reducer } from "./store.reducer"

// react context store
const TodoContext = React.createContext(null)

// create redux state selector and dispatch from context
export const useTodoStore = createStoreHook(TodoContext)
export const useTodoDispatch = createDispatchHook(TodoContext)
export const useTodoSelector = createSelectorHook(TodoContext)

// create redux store from the reducer
const todoStore = createStore(reducer)

// create store provider wrap subtree
export function TodoStoreProvider({ children }) {
  return (
    <Provider context={TodoContext} store={todoStore}>
      {children}
    </Provider>
  )
}
``` 

After creating a store provider we are going to create `store.reducer.js` where we define the reducer and actions for the store.

```jsx
// [filename: todo.reducer.js]

export const loadNext = () => ({ type: 'load_next' });
export const addTodos = ({ todos, total }) => ({ type: 'add_todos', payload: { todos, total } });
export const setLoading = (loading) => ({ type: 'set_loading', payload: { loading }  });

const InitState = {
 status: 'idle', // idle | pending | resolve | reject 
 todos: [],
 total: 0,
 skip: 0,
 limit: 10
};

export const reducer = (state = InitState, action) => {
  switch (action.type) {
    case 'load_next': {
       if (state.todos.length < state.total && state.status !== 'pending') {
          return {
             ...state,
             status:  'pending'
          };
       }
       return state;
    }
    case 'add_todos': {
      return {
          ...state,
          status: 'resolve',
          todos: [...state.todos, ...action.payload.todos],
          total: state.total + action.payload.todos.length 
      };
    }
    case 'set_loading': {
      return {
          ...state,
          status: action.payload.loading
      };
    }
    default: {
      return state;
    }
  }
};
```
### [Loadable](#heading-loadableloadable)
Loadables are react components that wrap all data loading logic in it and update the store.
```jsx
// [filename: App.js]

const App = () => (
  <div>
    <TodoStoreProvider>
      {/* Loadable holds all data loading logic*/}
      <TodoLoadable>
        {/* Render todos */}
      </TodoLoadable>
     </TodoStoreProvider>
   </div>
 );
```
Now let's create a loadable:
```jsx
// [filename: Todo.loadable.js]

function TodoLoadable(props) {
  // react-redux state slice selector
  const skip = useTodoSelector((state) => state.skip);
  const limit = useTodoSelector((state) => state.limit);
  const todoDispatch = useTodoDispatch();
  // load data
  useEffect(() => {
    todoDispatch(setLoading('pending'));
    api({ skip, limit })
      .then((res) => todoDispatch({ todos: res.todos, total: res.total }))
      .catch((e) => todoDispatch(setLoading('reject')));
  }, [skip, limit]);
  // render child
  return <>{props.children}</>
}
```
The point to note here is that the loading logic is completely placed inside the loadable and the children can utilize the store to sync UI state accordingly. `IsVisible` is a utility component that can be used to render things conditionally.

```jsx
// [filename: IsVisible.utility.jsx]

function IsVisible({ visible, unmountOnExit, ...props }) {   
  if (unmountOnExit && !visible) {
    return null;
  }
  return <div {...props} style={{  ...props.style, display: visible ? 'flex' : 'none'  }} />
}
```
We can use the `IsVisible` utility component to create state synced UI.

```jsx
// [filename: Todo.jsx]

const Error = () => <div><h1>Error</h1></div>;
const Loader = () => <CircularProgress size="small" />
const Todos = () => {
  const todos = useTodoSelector((state) => state.todos);
  return <div>{todos.map((todo) => <h1>{todo}</h1>)}</div>
}

function IsErrorVisible(props) {
  const isError = useTodoSelector((state) => state.status === 'reject');
  return <IsVisible {...props} visible={isError} />
}

....more IsVisible for all API status 'reject' | 'resolve' | 'pending' | 'idle'
```

Now with the help of this `IsVisible`, we can render UI according to the state of API.

```jsx
// [filename: App.js]

const App = () => (
  <div>
    <TodoStoreProvider>
      {/* Loadable holds all data loading logic*/}
      <TodoLoadable>
        <IsErrorVisible><ErrorUI /></IsErrorVisible>
        <IsTodoVisible><Todos /></IsTodoVisible>
        <IsLoaderVisible><Loader /></IsLoaderVisible>
      </TodoLoadable>
     </TodoStoreProvider>
   </div>
 );
```

This is how `loadable` along with `IsVisible` utility makes it super easy to load data in react and make a code simple to write and understand. Here is a link to demo [Codesandbox.](https://codesandbox.io/s/loadables-9fc1j6)